拥有大量小方法是否有助于 JIT 编译器进行优化?
在最近关于如何优化某些代码的讨论中,我被告知将代码分解为许多小方法可以显着提高性能,因为JIT编译器不喜欢优化大型方法。
我不确定这一点,因为JIT编译器本身似乎应该能够识别自包含的代码段,无论它们是否在自己的方法中。
任何人都可以证实或反驳这一说法吗?
在最近关于如何优化某些代码的讨论中,我被告知将代码分解为许多小方法可以显着提高性能,因为JIT编译器不喜欢优化大型方法。
我不确定这一点,因为JIT编译器本身似乎应该能够识别自包含的代码段,无论它们是否在自己的方法中。
任何人都可以证实或反驳这一说法吗?
热点 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:+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
您还可以检查生成的程序集以确认未内联(我使用了这些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)
因此,内联到 ,您会期望这样我们就可以回到原始场景。但是当被编译时,它实际上内联了整个事情。在程序集级别,这意味着您将不再找到任何说明。你会发现这样的行: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)
其中,基本上常见的指令是“相互化的”。
结论
我并不是说这个例子具有代表性,但它似乎证明了几点:
最后:如果代码的一部分对于性能确实至关重要,并且这些注意事项很重要,则应检查 JIT 输出以微调代码,并分析之前和之后的重要内容。
如果您采用完全相同的代码,并将它们分解为许多小方法,那对JIT根本没有帮助。
一个更好的说法是,现代HotSpot JVM不会因为你编写了很多小方法而惩罚你。它们确实被积极地内联,因此在运行时,您并没有真正支付函数调用的成本。即使对于调用虚拟调用(如调用接口方法的调用)也是如此。
几年前,我写了一篇博客文章,描述了如何看到JVM正在内联方法。该技术仍然适用于现代JVM。我还发现,看看与 invokedynamic 相关的讨论会很有用,在讨论中,现代 HotSpot JVM 如何编译 Java 字节代码得到了广泛的讨论。