C# 编译器会优化这段代码吗?
Posted
技术标签:
【中文标题】C# 编译器会优化这段代码吗?【英文标题】:Will the C# compiler optimize this code? 【发布时间】:2014-02-18 20:51:23 【问题描述】:我经常遇到这种情况。乍一看,我想,“那是糟糕的编码;我正在执行一个方法两次,必然会得到相同的结果。”但是想到这里,我不得不怀疑编译器是否和我一样聪明,并且可以得出相同的结论。
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
方法的内容?说它看起来像这样(有点类似于我现在的真实代码):
public OtherThing GetOtherThing()
if (this.Category == null) return null;
return this.Category.OtherThings.FirstOrDefault(t => t.Text == this.Text);
除非对这些对象来自的任何存储进行非常糟糕的异步更改处理,否则如果连续运行两次,肯定会返回相同的内容。但是如果它看起来像这样(为了论证而荒谬的例子)怎么办:
public OtherThing GetOtherThing()
return new OtherThing
Id = new Random().Next(100)
;
连续运行两次将导致创建两个不同的对象,很可能具有不同的 ID。在这些情况下编译器会做什么?它是否像我在第一个列表中展示的那样低效?
自己做一些工作
我运行了与第一个代码清单非常相似的东西,并在GetOtherThing
实例方法中放置了一个断点。断点被击中一次。所以,看起来结果确实被缓存了。在第二种情况下会发生什么,该方法每次可能返回不同的东西?编译器会错误地优化吗?我发现的结果有什么警告吗?
编辑
那个结论是无效的。请参阅@usr 的答案下的 cmets。
【问题讨论】:
我相信方法结果存储在本地调用堆栈中并被引用,而不是在启用编译器优化时两次调用该方法。 @DavidHaney 你为什么这么相信?您对堆栈的讨论描述了如果执行优化,IL 可能会是什么样子,但不是这种优化是否有效和可能。 正如您所说,方法结果在调用之间可能会有所不同,因此编译器无法将其优化为仅一次调用。但是,当您实际运行代码时,很有可能会由 JIT 执行优化。 @DavidHaney 这听起来像是一个不太可能且影响行为的优化。你能提供一个引用吗? @MarcinJuraszek 为什么 JIT 可能会执行此优化吗?虽然 JIT 可以轻松内联所涉及的方法,但我看不出有什么方法可以排除对x.Category.OtherThings
的并发修改,这会影响结果。
【参考方案1】:
这里需要考虑两个编译器:将 C# 转换为 IL 的 C# 编译器,以及将 IL 转换为机器代码的 IL 编译器 - 称为 jitter,因为它发生在 Just In Time 中。
Microsoft C# 编译器当然没有这种优化。方法调用生成为方法调用,故事结束。
允许抖动执行您描述的优化只要无法检测到这样做。例如,假设您有:
y = M() != 0 ? M() : N()
和
static int M() return 1;
抖动是允许把这个程序变成:
y = 1 != 0 ? 1 : N()
或就此而言
y = 1;
抖动是否这样做是一个实现细节;如果您关心,您必须询问抖动专家是否确实执行此优化。
同样,如果你有
static int m;
static int M() return m;
然后抖动可以优化成
y = m != 0 ? m : N()
甚至进入:
int q = m;
y = q != 0 ? q : N();
因为允许抖动将连续的两个字段读取转换为单个字段读取,前提是该字段不是易失性的。同样,是否这样做是一个实现细节;询问抖动开发人员。
但是,在后一个示例中,抖动不能忽略第二次调用,因为它有副作用。
我运行了与第一个代码清单非常相似的东西,并在 GetOtherThing 实例方法中放置了一个断点。断点被命中一次。
这是极不可能的。调试时几乎所有优化都关闭了,正是为了便于调试。作为Sherlock Holmes never said,当你排除不可能的时候,最有可能的解释是原发帖人弄错了。
【讨论】:
【参考方案2】:只有在您无法区分差异时,编译器才能应用优化。在您的“随机”示例中,您可以清楚地分辨出区别。它不能以这种方式“优化”。这将违反 C# 规范。事实上,规范并没有太多地谈论优化。它只是说你应该观察程序做什么。在这种情况下,它指定应该抽取两个随机数。
在第一个示例中,可以应用此优化。它永远不会在实践中发生。以下是一些使事情变得困难的事情:
查询操作的数据可以通过您的虚函数调用更改,或者您的 lambda (t => t.Text == this.Text
) 可以更改列表。非常阴险。
它可以被另一个线程更改。我不确定 .NET 内存模型对此有何看法。
它可以通过反射来改变。
必须证明计算将始终返回相同的值。你将如何证明这一点?您需要分析所有可能运行的代码。包括虚拟调用和依赖数据的控制流。
所有这些都必须跨非内联方法和程序集工作。
C# 编译器无法执行此操作,因为它无法查看 mscorlib。补丁版本可能随时更改 mscorlib。
JIT 是一个糟糕的 JIT(唉),它针对编译速度进行了优化(唉)。它不这样做。如果您怀疑当前的 JIT 是否会进行一些高级优化,可以肯定的是它不会。
【讨论】:
这听起来是一个很好的答案,那么您如何解释我的断点只被命中一次的事实?看起来优化确实发生了。 也许你去了: 0
分支,或者你犯了其他错误。将Console.WriteLine
调用到所有 3 个位置(Write() ? Write() : Write()
用于一些有用的写入功能)。你总是会看到两个电话。在发布模式下,有时由于优化(与此处讨论的所谓优化无关)而没有命中断点。
哦,呃,你是对的,在这种情况下,它是三元语句的“假”面。遗憾的是,我的代码结构在过去半小时内发生了变化,因此目前不利于进一步测试。我会相信你的话,下次出现这种情况时,我一定会进行更多测试。谢谢!以上是关于C# 编译器会优化这段代码吗?的主要内容,如果未能解决你的问题,请参考以下文章