JIT如何内联我们的C#代码(启发式)

内联是编译器中最重要的优化之一。它不仅消除了调用的开销,而且还为其他优化打开了许多可能性,例如恒定折叠,消除死代码等。此外,有时内联会导致调用函数的大小减小!我问几个人是否知道按什么规则内联C#中的函数,并且大多数人回答说JIT着眼于IL代码的大小,并且仅内联大小不超过32个字节的小函数。因此,我决定写这篇文章,以仅使用这样一个示例来公开实现细节,该示例将立即展示几种启发式方法:




您认为对Volume构造函数的调用会在此内联吗?明显不是。它太大了,特别是由于重量级的运算符的抛出,导致了相当大胆的代码生成。让我们签入Disasmo:



Inline!此外,所有异常及其分支均已成功删除!您可以这样说:“啊,好吧,Jit很聪明,并且对所有内联候选人进行了全面分析,看了一下如果通过特定参数会发生什么情况”或“ Jit试图内联所有可能的东西,执行所有优化,然后从中获利(是否选择组合)(例如,组合十种方法或两种方法的调用图,并计算该操作的复杂性)。

好吧...不,这是不现实的,特别是在及时方面。因此,大多数编译器使用所谓的观察和启发式方法来解决这个经典的背包问题,并尝试确定自己的预算并尽可能有效地适应预算(而且,PGO并非万能药)。 RyuJIT有正面和负面的观察。正数增加收益系数(收益乘数)。系数越高,我们可以内联的代码越多。相反,负面的看法-降低它甚至禁止内联。让我们看看RyuJIT对我们的示例进行了哪些观察:



可以在COMPlus_JitDump的日志中看到这些观察(例如,在Disasmo中):



所有这些简单的观察使系数从1.0开始增加11.5,并成功地克服了内联器的预算,例如,我们传递了一个常量参数并将其与另一个常量进行比较的事实告诉我们,在折叠常量之后,很有可能将条件分支之一删除,并且代码将变得更小。或者,例如,这是一个构造函数并且在循环内被调用的事实,也暗示了jit应该软化内联的要求。

除了收益乘数之外,RyuJIT还使用观察值来预测本机功能代码的大小及其性能影响,其中使用通过ML获得的EstimateCodeSize()EstimatePerformanceImpact()中的魔术常数

顺便说一句,您是否注意到了这个技巧?:

if ((value - 'A') > ('Z' - 'A'))

这是针对以下情况的优化版本:

if (value < 'A' || value > 'Z')

这两个表达式是相同的,但是在第一种情况下,我们有一个基本单位,在第二种情况下,我们有三个基本单位。事实证明,内联函数对函数中基本块的数量有严格限制,如果超过5个,则我们的收益乘数有多大都没关系-内联被取消。因此,我应用了此技巧以适应此严格要求。如果罗斯林为我做的很好。

问题罗斯林github.com/dotnet/runtime/issues/13347
PR在RyuJIT(我笨拙地企图):github.com/dotnet/coreclr/pull/27480

我描述了为什么它是有道理的例子做不仅日新但并在C#编译器中。

内联和虚拟方法


这里的一切都很清楚,在编译阶段您无法内联没有任何信息,尽管如果类型或方法是密封的,那么为什么不这样做

内联和引发异常


如果某个方法从不返回任何值(例如,它只是throw new...),则此类方法会自动标记为throw-helpers,并且不会内联。这样可以从throw new地毯下面扫掠复杂的代码源并安抚内衬。

内联和[AggressiveInlining]属性


在这种情况下,建议内联程序内联该方法,但是出于两个原因,您必须格外小心:

  • 也许您优化了一种情况,而使其他所有情况恶化了(例如,改进了常量参数的情况),这取决于代码源的大小。
  • 内联通常会生成大量的临时变量,这些临时变量可能超过某个限制-RyuJIT可以跟踪其生命周期的变量数量(512),此后,代码将开始增长为堆栈上的可怕溢出并大大减慢速度。两个很好的例子:tytstyts

内联和动态方法


当前,此类方法不能内联,也不能自行内联:github.com/dotnet/runtime/issues/34500

我尝试写启发式


最近,我尝试编写自己的启发式方法来在这种情况下提供帮助:



在上一篇文章中,我提到我最近优化了RyuJIT来计算常量字符串的长度("Hello".Length -> 5我们看到如果是zainlaynit),因此,在上面的^ Validatein中Test,我们得到if ("hello".Length > 10)什么是在优化if (5 > 10)什么是在移除整个条件/分支的最优化。但是,内联函数拒绝内联Validate



并且这里的主要问题是还没有启发式方法告诉jit我们正在将常量字符串传递给System.String::get_Length,这意味着callvirt-call很可能会崩溃为常量,并且整个分支都将被删除。其实我的启发式并添加此观察结果(唯一的不足是您必须解决所有callvirts,这不是很快)。

还有其他限制,可以在此处找到其一般列表在这里,您可以阅读一位主要的JIT开发人员对衬板设计的想法,以及在此案例中有关使用机器学习的文章

All Articles