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

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

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

任何人都可以证实或反驳这一说法吗?


答案 1

热点 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;
    }
}

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

您将看到它并被编译,但不会内联: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

您还可以检查生成的程序集以确认未内联(我使用了这些JVM标志:) - 它看起来像这样:m-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)

因此,内联到 ,您会期望这样我们就可以回到原始场景。但是当被编译时,它实际上内联了整个事情。在程序集级别,这意味着您将不再找到任何说明。你会发现这样的行:m2mmaininvokevirtual

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

其中,基本上常见的指令是“相互化的”。

结论

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

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

最后:如果代码的一部分对于性能确实至关重要,并且这些注意事项很重要,则应检查 JIT 输出以微调代码,并分析之前和之后的重要内容。


答案 2

如果您采用完全相同的代码,并将它们分解为许多小方法,那对JIT根本没有帮助。

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

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


推荐