什么条件下CLR可以将方法内联?

42

我观察到许多应用程序中存在很多"堆栈内省"的代码,这些代码通常会隐式依赖于其包含方法不被内联以确保其正确性。这些方法通常涉及以下调用:

  • MethodBase.GetCurrentMethod
  • Assembly.GetCallingAssembly
  • Assembly.GetExecutingAssembly

现在,我发现这些方法周围的信息非常令人困惑。我听说运行时不会内联调用GetCurrentMethod的方法,但是我找不到任何相关文档。我在StackOverflow上看到了一些帖子,比如这个,表明CLR不会内联跨程序集的调用,但是GetCallingAssembly文档强烈表示相反。

还有备受诟病的[MethodImpl(MethodImplOptions.NoInlining)],但我不确定CLR是否认为这是一个"请求"还是一个"命令"。

请注意,我询问的是从契约角度来看内联的资格,而不是关于当当前JITter实现由于实现难度而拒绝考虑方法的情况,或者关于当JITter最终在评估权衡后选择内联一个符合条件的方法的情况。我阅读了这个这个,但它们似乎更注重于后面两点(虽然提到了MethodImpOptions.NoInlining和"异国情调的IL指令",但这些似乎被呈现为启发式而不是义务)。

何时允许CLR进行内联?


1
“CLR 何时允许内联?”我猜是在没有该属性的情况下。 - CodesInChaos
1
@CodeInChaos:你的意思是说,除非我们在方法上加上[MethodImpl(MethodImpOptions.NoInlining)]修饰符,否则Assembly.GetExecutingAssembly会出问题? - Ani
这里有一个不错的链接(虽然不能回答你所有的问题...):http://blogs.msdn.com/b/vancem/archive/2008/08/19/to-inline-or-not-to-inline-that-is-the-question.aspx - Simon Mourier
嗯,听起来像是一个规范上的错误:我理解以其他方式执行的技术上的便利性,但显然GetCallingAssembly和相关函数不应该被规定为敏感于内联...叹气 - 而GetExecutingAssembly的文档根本没有提到内联! - Eamon Nerbonne
这不是你问题的答案,但从Visual Studio 2012开始,使用编译器的帮助可以通过Caller Information属性获取调用者方法/属性名称、其文件位置和行号,即不受JIT内联的影响。此外,与堆栈(帧创建和随后的)检查相比,它非常快速。 - Evgeniy Berezovsky
5个回答

25
这是一个细节实现问题,x86和x64的JIT编译器有微妙的不同规则。这在参与JIT编译器开发的团队成员的博客文章中有简要记录,但团队当然保留修改规则的权利。看起来你已经找到了这些规则。
从其他程序集内联方法是完全支持的,如果不支持,很多.NET类将无法正常工作。当你查看为Console.WriteLine()生成的机器代码时,你可以看到它的工作方式,当你传递一个简单的字符串时,它经常被内联。要亲自验证这一点,你需要切换到发布版本并更改调试器选项。工具 > 选项 > 调试 > 通用,取消选中“在模块加载时抑制JIT优化”。
除此之外,没有什么好理由认为MethodImpOptions.NoInlining是有问题的,它基本上是为了这个目的而存在的。实际上,在.NET框架中,它被故意用于许多调用内部辅助方法的小型公共方法。这使得异常堆栈跟踪更容易诊断。

从你所说的,我可以推断出 void M()``{Console.WriteLine(MethodBase.GetCurrentMethod().Name);} 不保证输出 M,但是如果我用 NoInlining 装饰它,则保证输出 M。那么 Assembly.GetCallingAssemblyAssembly.GetExecutingAssembly 呢?虽然意图是“这个程序集”,但我很少看到调用 Assembly.GetExecutingAssembly 的代码被装饰上属性。 - Ani
1
是的,如果包含这样代码的方法是公共的,那么它应该使用该属性进行修饰。它可能因为方法的规模而幸存下来了。 - Hans Passant

8
尽管Hans Passant的答案有所不同,但此处提供了一些2004年以来的提示,还有更多最新信息。它们可能会发生变化,但是如果要使方法符合内联资格,它们确实可以让您了解要查找的内容:

JIT不会内联:

  • 被标记为“ MethodImplOptions.NoInlining”的方法
  • 大于32字节的IL方法
  • 虚拟方法
  • 带有大值类型参数的方法
  • MarshalByRef类上的方法
  • 具有复杂控制流程的方法
  • 满足其他更奇特的条件的方法

特别地,MethodImplOptions.AggressiveInlining被认为是可以解除32字节限制(或者针对您的平台和目前情况),

.Net 3.5添加了启发式技术,帮助确定是否内联,这可能是一件好事,尽管它使开发人员更难预测JIT的决策:

下面是文章中的引用:

如果内联使得代码比替换的调用更小,则内联是绝对有益的。请注意,我们谈论的是本地代码大小,而不是 IL 代码大小(这可能会相差很大)。
特定调用站点执行次数越多,就越受内联的好处。因此,循环中的代码应该比不在循环中的代码更容易被内联。
如果内联暴露了重要的优化,则内联更为可取。特别是由于像这样的优化而具有值类型参数的方法更具优势,因此具有倾向性地将这些方法内联良好。
因此,X86 JIT 编译器使用的启发式规则是,在给定一个内联候选者后:
- 如果未内联该方法,则估算调用站点的大小。 - 估计如果将其内联,则调用站点的大小(这是基于 IL 的估计,我们使用一个简单的状态机(马尔可夫模型),使用大量真实数据来形成此估计逻辑) - 计算乘数。默认情况下是 1。 - 如果代码在循环中(当前启发式规则在循环中将其增加到 5),则增加乘数。 - 如果看起来结构体优化将发挥作用,则增加乘数。 - 如果 InlineSize ≤ NonInlineSize * Multiplier,请进行内联操作。

3

4
我不同意;这是一个实现细节,目前恰好是正确的。 ["我们可能可以在这方面做得更好(例如,如果99%的调用最终都进入相同的目标,那么您可以生成代码,在虚拟调用要执行的对象的方法表上进行检查,如果不是99%的情况,则进行调用,否则只需执行内联代码)。"]。http://blogs.msdn.com/b/davidnotario/archive/2004/11/01/250398.aspx - Ani
偶然发现这个——并发现链接回到我的问题。 :-) - Frank V

1

有关MethodBase.GetCurrentMethod的内联更多信息,请参见此线程http://prdlxvm0001.codify.net/pipermail/ozdotnet/2011-March/009085.html

大致上来说,它指出RefCrawlMark不会停止调用方法的内联。但是,RequireSecObject确实会停止调用者的内联。

此外,Assembly.GetCallingAssembly和Assembly.GetExecutingAssembly方法没有此属性。


0

在2003年,MSDN上发布了一篇名为编写高性能托管应用程序的文章,清晰地概述了几个标准:

  • 大于32字节IL的方法将不会被内联。
  • 虚函数不会被内联。
  • 具有复杂流程控制的方法将不会被内联。复杂流程控制是指除if/then/else之外的任何流程控制;在这种情况下,是switch或while。
  • 包含异常处理块的方法不会被内联,但抛出异常的方法仍然是内联的候选对象。
  • 如果方法的任何形式参数是结构体,则该方法将不会被内联。

Sacha Goldshtein在2012年的博客文章CLR中的积极内联中提供了许多相同的建议。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接