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# 编译器会优化这段代码吗?的主要内容,如果未能解决你的问题,请参考以下文章

C ++编译器优化和短路评估[重复]

编译器会优化和重用变量吗

从 C# 应用程序内部调用 Matlab 编译器会引发异常

编译器会优化未使用的链接文件吗?

这段代码可以编译吗? [复制]

C#编译器优化那点事