拥有许多小方法是否有助于JIT编译器进行优化?

在最近关于如何优化某些代码的讨论中,我被告知将代码分解为许多小方法可以显着提高性能,因为JIT编译器不喜欢优化大型方法。

我不确定这一点,因为看起来JIT编译器本身应该能够识别自包含的代码段,而不管它们是否在自己的方法中。

任何人都可以确认或反驳这种说法吗?

Hotspot JIT仅内联小于特定(可配置)大小的方法。 因此,使用较小的方法可以实现更多的内联,这是很好的。

请参阅此页面上的各种内联选项。


编辑

详细说明:

  • 如果一个方法很小,它将被内联,因此很少有机会因为在小方法中拆分代码而受到惩罚。
  • 在某些情况下,拆分方法可能会导致更多内联。

示例 (如果您尝试,则完整代码具有相同的行号)

package javaapplication27; public class TestInline { private int count = 0; public static void main(String[] args) throws Exception { TestInline t = new TestInline(); int sum = 0; for (int i = 0; i < 1000000; i++) { sum += tm(); } System.out.println(sum); } public int m() { int i = count; if (i % 10 == 0) { i += 1; } else if (i % 10 == 1) { i += 2; } else if (i % 10 == 2) { i += 3; } i += count; i *= count; i++; return i; } } 

使用以下JVM标志运行此代码时: -XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:FreqInlineSize=50 -XX:MaxInlineSize=50 -XX:+PrintInlining (是的,我使用的值certificate了我的情况: m是太大但重构的mm2都低于阈值 - 其他值可能会得到不同的输出)。

您将看到m()main()被编译,但m()没有内联:

  56 1 javaapplication27.TestInline::m (62 bytes) 57 1 % javaapplication27.TestInline::main @ 12 (53 bytes) @ 20 javaapplication27.TestInline::m (62 bytes) too big 

您还可以检查生成的程序集以确认m未内联(我使用了这些JVM标志: -XX:+PrintAssembly -XX:PrintAssemblyOptions=intel ) - 它将如下所示:

 0x0000000002780624: int3 ;*invokevirtual m ; - javaapplication27.TestInline::main@20 (line 10) 

如果你像这样重构代码(我已经在一个单独的方法中提取了if / else):

 public int m() { int i = count; i = m2(i); i += count; i *= count; i++; return i; } public int m2(int i) { if (i % 10 == 0) { i += 1; } else if (i % 10 == 1) { i += 2; } else if (i % 10 == 2) { i += 3; } return i; } 

您将看到以下编译操作:

  60 1 javaapplication27.TestInline::m (30 bytes) 60 2 javaapplication27.TestInline::m2 (40 bytes) @ 7 javaapplication27.TestInline::m2 (40 bytes) inline (hot) 63 1 % javaapplication27.TestInline::main @ 12 (53 bytes) @ 20 javaapplication27.TestInline::m (30 bytes) inline (hot) @ 7 javaapplication27.TestInline::m2 (40 bytes) inline (hot) 

所以m2被内联到m ,你会期望我们回到最初的场景。 但是当main编译时,它实际上是整个内容。 在汇编级别,这意味着您将不再找到任何invokevirtual指令。 你会发现这样的行:

  0x00000000026d0121: add ecx,edi ;*iinc ; - javaapplication27.TestInline::m2@7 (line 33) ; - javaapplication27.TestInline::m@7 (line 24) ; - javaapplication27.TestInline::main@20 (line 10) 

基本上常见的指令是“共同的”。

结论

我并不是说这个例子具有代表性,但似乎certificate了几点:

  • 使用较小的方法可提高代码的可读性
  • 通常会内联较小的方法,因此您很可能不会支付额外方法调用的成本(它将是性能中立的)
  • 使用较小的方法可能会在某些情况下改善全局内联,如上例所示

最后:如果您的代码的一部分对于这些考虑因素至关重要的性能至关重要,那么您应该检查JIT输出以微调您的代码并重要地分析前后的配置文件。

如果您使用完全相同的代码并将它们分解为许多小方法,那么根本不会帮助JIT。

更好的方法是,现代的HotSpot JVM不会因为编写很多小方法而惩罚你。 它们确实得到了积极的内联,所以在运行时你并没有真正支付函数调用的成本。 甚至对于调用虚拟调用(例如调用接口方法的调用)也是如此。

几年前我做了一篇博客文章 ,描述了如何看待JVM是内联方法。 该技术仍适用于现代JVM。 我还发现查看与invokedynamic相关的讨论很有用,其中广泛讨论了现代HotSpot JVM如何编译Java字节代码。

我真的不明白它是如何工作的,但基于AurA提供的链接 ,我猜想如果重用相同的位,JIT编译器必须编译更少的字节码,而不是必须编译相似的不同字节码不同的方法。

除此之外,您越能够将代码分解为多个意义,您将从代码中获得更多的重用,这将允许对运行它的VM进行优化(您提供了更多的模式跟…共事)。

但是,我怀疑如果你破坏你的代码没有任何不提供代码重用的意义,它将会产生任何好的影响。