关于.net:C#编译器会优化这段代码吗?

Will the C# compiler optimize this code?

我经常遇到这种情况。乍一看,我想,"这是糟糕的编码;我正在执行一个方法两次,必然会得到相同的结果。"但我想,我不得不怀疑编译器是否和我一样聪明,是否能得出相同的结论。

1
2
3
4
var newList = oldList.Select(x => new Thing {
    FullName = String.Format("{0} {1}", x.FirstName, x.LastName),
    OtherThingId = x.GetOtherThing() != null : x.GetOtherThing().Id : 0 // Might call x.GetOtherThing() twice?
});

编译器的行为是否取决于GetOtherThing方法的内容?假设它看起来像这样(有点像我现在的代码):

1
2
3
4
public OtherThing GetOtherThing() {
    if (this.Category == null) return null;
    return this.Category.OtherThings.FirstOrDefault(t => t.Text == this.Text);
}

这样,除非对这些对象所来自的存储区进行了非常糟糕的异步更改,否则如果连续运行两次,肯定会返回相同的结果。但是,如果它看起来像这样(为了争论的荒谬的例子):

1
2
3
4
5
public OtherThing GetOtherThing() {
    return new OtherThing {
        Id = new Random().Next(100)
    };
}

一行运行两次会导致创建两个不同的对象,很可能具有不同的ID。编译器在这些情况下会做什么?它是否像我在第一个清单中显示的那样效率低下?

我自己做一些工作

我运行了与第一个代码列表非常相似的代码,并在GetOtherThing实例方法中放置了一个断点。断点被命中一次。所以,看起来结果确实是缓存的。在第二种情况下会发生什么,方法每次可能返回不同的内容?编译器是否会错误地优化?对我发现的结果有什么警告吗?

编辑

那个结论是无效的。参见@usr's answer下的评论。


这里有两个编译器需要考虑:将C转换为IL的C编译器和将IL转换为机器代码的IL编译器,称为抖动,因为它是及时发生的。

微软C编译器当然不会进行这种优化。方法调用生成为方法调用,即故事的结尾。

如果检测不到抖动,则允许抖动执行您描述的优化。例如,假设您有:

1
y = M() != 0 ? M() : N()

1
static int M() { return 1; }

允许抖动将此程序转换为:

1
y = 1 != 0 ? 1 : N()

或是为了那件事

1
y = 1;

抖动是否这样做是一个实现细节;如果您愿意,您必须询问抖动专家它是否真的执行了这种优化。

同样,如果你有

1
2
static int m;
static int M() { return m; }

然后抖动可以将其优化为

1
y = m != 0 ? m : N()

甚至进入:

1
2
int q = m;
y = q != 0 ? q : N();

因为允许抖动将一行中的两个字段读操作转换成一个字段读操作,而不需要进行中间写入操作,前提是字段不易失。同样,它是否这样做是一个实现细节;询问一个不安的开发人员。

但是,在后一个示例中,抖动不能消除第二个调用,因为它有副作用。

I ran something very similar to that first code listing and put a breakpoint in the GetOtherThing instance method. The breakpoint was hit once.

这是极不可能的。几乎所有的优化都在调试时关闭,这样更容易调试。正如夏洛克·福尔摩斯从未说过的那样,当你消除了不可能发生的事情,最有可能的解释是原来的海报是错的。


只有当您无法分辨差异时,编译器才能应用优化。在你的"随机"例子中,你可以清楚地分辨出不同之处。它不能这样"优化"。这将违反C规范。事实上,规范并没有谈论很多优化。它只是说你应该观察程序的操作。在这种情况下,它指定应该绘制两个随机数。

在第一个示例中,可能可以应用此优化。这在实践中永远不会发生。以下是一些让事情变得困难的事情:

  • 查询操作的数据可以由您的虚拟函数调用更改,或者您的lambda(t => t.Text == this.Text可以更改列表。非常阴险。
  • 它可以被另一个线程更改。我不知道.NET内存模型是怎么说的。
  • 它可以通过反射来改变。
  • 必须证明计算总是返回相同的值。你将如何证明这一点?您需要分析所有可能运行的代码。包括虚拟调用和与数据相关的控制流。

所有这些都必须在非内联方法和程序集之间工作。

C编译器不能这样做,因为它不能查看mscorlib。补丁发布可能随时改变mscorlib。

JIT是一个糟糕的JIT(遗憾),它针对编译速度(遗憾)进行了优化。它不能做到这一点。如果您不确定当前的JIT是否会进行一些高级优化,那么可以放心,它不会。