如何证明.NET CLR JIT在每次运行时只编译每个方法一次?

3

有一个旧问题询问C#是否每次都进行JIT编译,著名的Jon Skeet回答说:“只要我们谈论的是没有NGENed的桌面应用程序,它只会编译一次”。我想知道这个2009年的信息是否仍然正确,我想通过实验和调试来弄清楚这一点,可能需要在JITter上设置断点,并使用WinDbg命令检查对象和方法。

到目前为止我的研究

我知道.NET内存布局考虑了每个对象的头(位于地址A-4)和方法表(位于地址A+0),然后才是实际数据(位于地址A+4)。因此,每个对象都可能具有不同的方法表,因此可以具有不同的JIT方法。

为什么我对该语句的正确性表示怀疑?

我们进行了并行编程的研讨会,培训师声称每个线程的每个对象都会进行JIT编译。这显然对我来说毫无意义,我能够编写一个反例应用程序。

很不幸,以下其他主题也出现了,我也想写一个演示:

  • 新的.NET框架
  • 应用程序域
  • 代码访问安全性

链接的答案是在.NET 3.5发布时编写的。自那以后它没有实质性地改变过,即它没有为.NET 4.0、4.6和4.6接收更新。

关于应用程序域,我个人认为我可以卸载应用程序域,这将卸载程序集。如果一个程序集被卸载了,它就不存在了,IL代码也随之消失。因此,我可以想象创建一个应用程序域并重新加载程序集可能会导致再次JIT方法。

关于代码访问安全性,我不确定JIT编译器是否根据当前权限考虑它,或者它是否在运行时通过反射完成。如果它是由JIT编译器完成的,那么编译后的代码将有所不同,取决于权限集。


1
我不明白代码访问安全性如何改变将IL转换为实际代码的方式,我认为新框架并没有改变关于何时JIT的决策(但引入了新的JIT编译器)。但是你提到的应用程序域似乎提出了一个真正的新方面,这是尚未解决的。 - Damien_The_Unbeliever
3
对我来说,为什么需要证明这一点并不是很明显。该声明确实不正确(躲闪闪电的攻击),通用代码会被JIT编译器编译多次。一个副本处理任何引用类型参数,另外为每个不同的值类型参数创建额外的副本。当然,对于应用程序域,LoaderOptimization适用。有分析器回调函数告诉您JIT正在处理什么,ICorProfilerCallback::JITCompilationStarted()和ICorProfilerCallback4::ReJITCompilationStarted()。 - Hans Passant
1
除了Hans的评论之外,在.NET 4.5中,托管分析器可以请求运行时重新编译方法(以便可以以不同的方式进行检测)。还有一种情况是,运行时可以在正在编译的过程中开始编译一个方法。 - Brian Reichle
1
使用ETW来捕获.NET Jit事件(https://dev59.com/CIrda4cB1Zd3GeqPS_T4#30289933)。在配置文件中,将0x8008替换为0x8018以捕获Jit事件。现在您可以看到带有调用堆栈的加载器、Jit和异常数据。现在观察Jit何时发生。 - magicandre1981
@MatíasFidemraizer:我看过了,可以看到他在链接的答案中留下了评论。看起来他没有足够的知识进行更新。 - Thomas Weller
显示剩余4条评论
1个回答

2
JIT的“设计原则”是编译一个方法(对于泛型方法实例化)一次,并重复使用相同的本机代码。当然,实现非常复杂,我会尝试简化答案而不影响准确性。自.NET 2.0以来,所有版本的运行时都是相同的,直到最新的.NET 4.6(我不知道.NET 1.x,可能也是这样)。
运行时分析器回调和 ETW 事件的文档不够完善。它们都在尝试 JIT 编译时发生,但并不一定成功。有三种情况可能会发生这种情况:1- 方法未能满足某些安全要求,2- 方法的验证失败,3- 无法分配内存来保存要发出的本机代码。因此,JIT 启动回调和事件可能会高估方法实际编译的次数。同样地,JIT 完成回调和事件也不准确。很少有情况下它们可能会低估同一方法成功编译的次数。值得一提的是,此时 # of Methods JITted performance counter 准确报告了所有 IL 方法在进程的所有应用程序域中共同编译的次数。
对于特定于应用程序域的程序集,方法在每个应用程序域中单独编译。没有共享(尽管有时可能在技术上是可能的)。对于特定于应用程序域的程序集,运行时会尝试编译每个方法一次,并与所有应用程序域共享本机代码。
如何证明.NET CLR JIT在每次运行中只编译每个方法一次?
在某些情况下(例如使用后台JIT和其他极其微妙的情况),某个方法可能会在没有执行的情况下被编译。因此,说每个方法在每次运行中都被编译一次是不准确的。
您可以参考CoreCLR JIT源代码以获取更多信息(JIT与.NET Framework 4.5+中使用的JIT相同,但本答案适用于旧版本,因为JIT触发机制基本相同)。源代码就是证明。
“每个对象每个线程都会被JIT编译”是没有意义的。编译的范围是应用程序域。

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