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

san*_*ity 42 java optimization jit compiler-optimization

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

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

任何人都可以确认或驳斥这一说法吗?

ass*_*ias 30

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 += t.m();
        }
        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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

您将看到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
Run Code Online (Sandbox Code Playgroud)

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

0x0000000002780624: int3   ;*invokevirtual m
                           ; - javaapplication27.TestInline::main@20 (line 10)
Run Code Online (Sandbox Code Playgroud)

如果你像这样重构代码(我在一个单独的方法中提取了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;
}
Run Code Online (Sandbox Code Playgroud)

您将看到以下编译操作:

 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)
Run Code Online (Sandbox Code Playgroud)

因此,您可以期待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)
Run Code Online (Sandbox Code Playgroud)

基本上常见的指令是"共同的".

结论

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

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

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


Koh*_*chi 7

如果你采用完全相同的代码并将它们分解成许多小方法,那根本不会帮助JIT.

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

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


Ben*_*kay 1

我不太明白它是如何工作的,但根据AurA 提供的链接,我猜测如果重用相同的位,JIT 编译器将不得不编译更少的字节码,而不是必须编译跨域相似的不同字节码不同的方法。

除此之外,你越能够将代码分解成有意义的片段,你就能从代码中获得更多的重用,这将允许优化运行它的虚拟机(你提供了更多的模式)跟...共事)。

但是,我怀疑如果您在没有任何意义且不提供代码重用的情况下分解代码,是否会产生任何良好的影响。