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? }); |
编译器的行为是否取决于
1 2 3 4 | public OtherThing GetOtherThing() { if (this.Category == null) return null; return this.Category.OtherThings.FirstOrDefault(t => t.Text == this.Text); } |
这样,除非对这些对象所来自的存储区进行了非常糟糕的异步更改,否则如果连续运行两次,肯定会返回相同的结果。但是,如果它看起来像这样(为了争论的荒谬的例子):
一行运行两次会导致创建两个不同的对象,很可能具有不同的ID。编译器在这些情况下会做什么?它是否像我在第一个清单中显示的那样效率低下?
我自己做一些工作我运行了与第一个代码列表非常相似的代码,并在
编辑
那个结论是无效的。参见@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是否会进行一些高级优化,那么可以放心,它不会。