请考虑以下代码:
public static void M() {
A(V);
A(V);
A(V);
}
public static void V() {
}
public static void A(Action x) {
x();
}
这可以在幕后编译:
public static void M() {
A(new Action(V));
A(new Action(V));
A(new Action(V));
}
但是,我们可以编写自己的简单性能改进,以减少不必要的垃圾:
private static readonly Action v = new Action(V);
A(v);
A(v);
A(v);
对于这个非常简单的案例,Roslyn是否有任何理由无法进行类似的优化?
如果答案是否定的,那么方法何时不是静态但实例成员呢?什么时候有关闭的变量被捕获?
答案 0 :(得分:17)
我们可以编写自己的简单性能改进,以减少不必要的垃圾
您重新发现了公共子表达式消除的特殊情况 - 优化识别两个或多个表达式具有完全相同的值,计算一次值,并将其存储在变量中重新使用。
在继续之前,我告诫你,所有所谓的“优化”实际上都是另一回事。您建议的优化会在每次调用时产生少量的收集压力,而不是内存泄漏。静态字段中的缓存值将成为gen 2堆的永久成员。那值得吗?这是一个你想通过实际测量来回答的问题。
对于这个非常简单的案例,Roslyn是否有任何理由无法进行类似的优化?
如果优化没有对程序的行为产生不可接受的改变,那么原则没有理由无法执行此优化。
特别是,优化会导致两个先前值相等但不等于引用的委托变为引用相等。这很可能是可以接受的。
实际上,实现优化需要花费大量精力来设计,实现,测试和维护执行优化的代码。 C#没有实现常见的子表达式消除优化。这种优化具有很差的优势。很少有人编写可以从优化中受益的代码,并且优化很小,如您所知,如果您愿意,很容易“手动”进行优化。
我注意到C#在lambda上做了类似的缓存。它不会执行常见的子表达式消除,但它只会生成一定的lambdas并缓存结果:
void M() { Action x = () => {}; ... }
生成就好像你写了:
static Action anon = null;
void M()
{
if (anon == null) anon = () => {};
Action x = anon;
...
如果答案是否定的,那么方法何时不是静态但实例成员呢?
如果优化没有对程序的行为产生不可接受的改变,那么原则没有理由无法执行此优化。
我注意到在这种情况下,需要进行优化以推断出实例当然是相同的。如果不这样做,将无法保持程序行为不得改变的不变性。
同样,在实践中,C#不会消除常见的子表达式。
当捕获封闭变量时会怎样?
被截获的是什么?你刚才谈到方法组转换给代表,显然现在我们正在谈论转换为代表的lambda。
C#规范明确指出编译器可以选择在相同的lambdas上进行公共子表达式消除,或者不相同。
如果优化没有对程序的行为产生不可接受的改变,那么原则没有理由不能执行。由于规范明确指出允许此优化,因此根据定义可以接受。
同样,在实践中,C#不会消除常见的子表达式。
也许你在这里注意到一种趋势。问题的答案“是这样的,允许这样的优化吗?”几乎总是“是的,如果它不会对程序的行为产生不可接受的变化”。但问题的答案是“C#在实践中实现了这样的优化吗?”通常没有。
如果您想了解编译器执行的优化的一些背景知识,I described them in 2009。
Roslyn在大多数情况下更好地完成了这些优化。例如,Roslyn在将临时值和局部变形为短暂而非持久变量方面做得更好。我完全重写了可空算术优化器; my eight-part series of articles describing how is here。而且还有很多改进。我们从未考虑过做CSE。
答案 1 :(得分:1)
你的问题有很多不同的组成部分和细微差别,所以我会尝试将它们分解一次。
首先,编译器通过语法糖执行魔术。如你所说,
public static void M() { A(V); }
相当于
public static void M() { A(new Action(V)); }
但编译器可以省去必须直接声明操作实例的麻烦。然而,在任何一种情况下,生成的IL都需要执行一系列步骤:
IL_000C: ldnull
IL_000D: ldftn UserQuery.V
IL_0013: newobj System.Action..ctor
IL_0018: call UserQuery.A
IL_0014: ldarg.0
IL_0015: ldarg.0
IL_0016: ldftn UserQuery.V
IL_001C: newobj System.Action..ctor
IL_0021: call UserQuery.A
IL_0027: ldarg.0
IL_0028: ldarg.0
IL_0029: ldftn UserQuery.V
IL_002F: newobj System.Action..ctor
IL_0034: call UserQuery.A
在指令V
处为我们的IL_000D
方法生成本机指针。前面的指令只是告诉我们该方法是静态的,否则我们会看到指令IL_000C: ldarg.0
,因为我们的实例方法参数需要被推送到评估堆栈。但是,在任何一种情况下,新的动作实例仍然需要在指令IL_0013: newobj
生成,因为我们传递方法 指针 (在引擎盖下),而不是方法 实例 。最后,一旦我们有了指针和新实例,我们就可以调用A
方法。
然而,在你的第二个例子中,事情发生了变化:
IL_0001: ldsfld UserQuery.v
IL_0006: call UserQuery.A
IL_000B: nop
IL_000C: ldsfld UserQuery.v
IL_0011: call UserQuery.A
IL_0016: nop
IL_0017: ldsfld UserQuery.v
IL_001C: call UserQuery.A
我们只需将静态字段v的值推送到ldsfld
指令中的评估堆栈,而不是生成指针或创建新对象。由于我们拥有该值,因此除了调用A
方法之外,我们不必执行任何其他操作。
再次,在我们的第二个例子中,为实例方法声明生成了一个附加指令,但它并没有改变参数的生成和传递方式,这是Roslyn不能优化的基本原因......编译器有义务生成运行时理解和期望的IL。尝试优化您的第一种情况,就像第二种情况一样,是一组根本不同的指令,因此无法对其进行优化。