Java 有很多小方法可以帮助JIT编译器优化,这是真的吗?
在最近一次关于如何优化某些代码的讨论中,我被告知将代码分解为许多小方法可以显著提高性能,因为JIT编译器不喜欢优化大方法 我不确定这一点,因为JIT编译器本身似乎应该能够识别自包含的代码段,而不管它们是否在自己的方法中Java 有很多小方法可以帮助JIT编译器优化,这是真的吗?,java,optimization,jit,compiler-optimization,Java,Optimization,Jit,Compiler Optimization,在最近一次关于如何优化某些代码的讨论中,我被告知将代码分解为许多小方法可以显著提高性能,因为JIT编译器不喜欢优化大方法 我不确定这一点,因为JIT编译器本身似乎应该能够识别自包含的代码段,而不管它们是否在自己的方法中 有人能证实或驳斥这一说法吗?我真的不明白它是如何工作的,但基于这一点,我猜如果重复使用相同的位,JIT编译器将不得不编译更少的字节码,而不是必须编译不同方法之间相似的不同字节码 除此之外,您越能将代码分解成不同的部分,代码的重用性就越高,这将允许对运行它的VM进行优化(您提供了更
有人能证实或驳斥这一说法吗?我真的不明白它是如何工作的,但基于这一点,我猜如果重复使用相同的位,JIT编译器将不得不编译更少的字节码,而不是必须编译不同方法之间相似的不同字节码 除此之外,您越能将代码分解成不同的部分,代码的重用性就越高,这将允许对运行它的VM进行优化(您提供了更多的模式)
然而,如果你在没有任何意义的情况下分解代码而不提供代码重用,我怀疑这会有什么好的影响。如果你把完全相同的代码分解成许多小方法,这对JIT毫无帮助 更好的说法是,现代热点JVM不会因为编写了很多小方法而惩罚您。它们会被积极地内联,因此在运行时,您不会真正支付函数调用的成本。即使是调用invokevirtual调用(例如调用接口方法的调用)也是如此
几年前我做了一篇文章,描述了如何看到JVM是内联方法。该技术仍然适用于现代JVM。我还发现查看与invokedynamic相关的讨论很有用,现代HotSpot JVM如何编译Java字节码得到了广泛的讨论。HotSpot JIT只内联小于特定(可配置)大小的方法。因此,使用较小的方法允许更多的内联,这是很好的 请参见上的各种内联选项
编辑 要详细说明一下:
- 如果一个方法很小,它将被内联,因此在小方法中拆分代码的机会很小
- 在某些情况下,拆分方法可能会导致更多的内联
m
未内联(我使用了以下JVM标志:-XX:+printsassembly-XX:printsassemblyoptions=intel
)-它将如下所示:
0x0000000002780624: int3 ;*invokevirtual m
; - javaapplication27.TestInline::main@20 (line 10)
0x00000000026d0121: add ecx,edi ;*iinc
; - javaapplication27.TestInline::m2@7 (line 33)
; - javaapplication27.TestInline::m@7 (line 24)
; - javaapplication27.TestInline::main@20 (line 10)
@Benchmark
public void smallFocusedMethods(TestState state) {
int i = state.value;
if (i < 90) {
actionOne(i, state);
} else {
actionTwo(i, state);
}
}
private void actionOne(int i, TestState state) {
state.sb.append(Integer.toString(i)).append(
": has triggered the first type of action.");
int result = i;
for (int j = 0; j < i; ++j) {
result += j;
}
state.sb.append("Calculation gives result ").append(Integer.toString(
result));
}
private void actionTwo(int i, TestState state) {
state.sb.append(i).append(" has triggered the second type of action.");
int result = i;
for (int j = 0; j < 3; ++j) {
for (int k = 0; k < 3; ++k) {
result *= k * j + i;
}
}
state.sb.append("Calculation gives result ").append(Integer.toString(
result));
}
Benchmark Mode Cnt Score Error Units
monolithicMethod thrpt 30 7609784.687 ± 118863.736 ops/s
monolithicMethod:·gc.alloc.rate thrpt 30 1368.296 ± 15.834 MB/sec
monolithicMethod:·gc.alloc.rate.norm thrpt 30 270.328 ± 0.016 B/op
monolithicMethod:·gc.churn.G1_Eden_Space thrpt 30 1357.303 ± 16.951 MB/sec
monolithicMethod:·gc.churn.G1_Eden_Space.norm thrpt 30 268.156 ± 1.264 B/op
monolithicMethod:·gc.churn.G1_Old_Gen thrpt 30 0.186 ± 0.001 MB/sec
monolithicMethod:·gc.churn.G1_Old_Gen.norm thrpt 30 0.037 ± 0.001 B/op
monolithicMethod:·gc.count thrpt 30 2123.000 counts
monolithicMethod:·gc.time thrpt 30 1060.000 ms
smallFocusedMethods thrpt 30 7855677.144 ± 48987.206 ops/s
smallFocusedMethods:·gc.alloc.rate thrpt 30 1404.228 ± 8.831 MB/sec
smallFocusedMethods:·gc.alloc.rate.norm thrpt 30 270.320 ± 0.001 B/op
smallFocusedMethods:·gc.churn.G1_Eden_Space thrpt 30 1393.473 ± 10.493 MB/sec
smallFocusedMethods:·gc.churn.G1_Eden_Space.norm thrpt 30 268.250 ± 1.193 B/op
smallFocusedMethods:·gc.churn.G1_Old_Gen thrpt 30 0.186 ± 0.001 MB/sec
smallFocusedMethods:·gc.churn.G1_Old_Gen.norm thrpt 30 0.036 ± 0.001 B/op
smallFocusedMethods:·gc.count thrpt 30 1986.000 counts
smallFocusedMethods:·gc.time thrpt 30 1011.000 ms
如果您像这样重构代码(我在一个单独的方法中提取了If/else):
您将看到以下编译操作:
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
指令。您会发现这样的行:
0x0000000002780624: int3 ;*invokevirtual m
; - javaapplication27.TestInline::main@20 (line 10)
0x00000000026d0121: add ecx,edi ;*iinc
; - javaapplication27.TestInline::m2@7 (line 33)
; - javaapplication27.TestInline::m@7 (line 24)
; - javaapplication27.TestInline::main@20 (line 10)
@Benchmark
public void smallFocusedMethods(TestState state) {
int i = state.value;
if (i < 90) {
actionOne(i, state);
} else {
actionTwo(i, state);
}
}
private void actionOne(int i, TestState state) {
state.sb.append(Integer.toString(i)).append(
": has triggered the first type of action.");
int result = i;
for (int j = 0; j < i; ++j) {
result += j;
}
state.sb.append("Calculation gives result ").append(Integer.toString(
result));
}
private void actionTwo(int i, TestState state) {
state.sb.append(i).append(" has triggered the second type of action.");
int result = i;
for (int j = 0; j < 3; ++j) {
for (int k = 0; k < 3; ++k) {
result *= k * j + i;
}
}
state.sb.append("Calculation gives result ").append(Integer.toString(
result));
}
Benchmark Mode Cnt Score Error Units
monolithicMethod thrpt 30 7609784.687 ± 118863.736 ops/s
monolithicMethod:·gc.alloc.rate thrpt 30 1368.296 ± 15.834 MB/sec
monolithicMethod:·gc.alloc.rate.norm thrpt 30 270.328 ± 0.016 B/op
monolithicMethod:·gc.churn.G1_Eden_Space thrpt 30 1357.303 ± 16.951 MB/sec
monolithicMethod:·gc.churn.G1_Eden_Space.norm thrpt 30 268.156 ± 1.264 B/op
monolithicMethod:·gc.churn.G1_Old_Gen thrpt 30 0.186 ± 0.001 MB/sec
monolithicMethod:·gc.churn.G1_Old_Gen.norm thrpt 30 0.037 ± 0.001 B/op
monolithicMethod:·gc.count thrpt 30 2123.000 counts
monolithicMethod:·gc.time thrpt 30 1060.000 ms
smallFocusedMethods thrpt 30 7855677.144 ± 48987.206 ops/s
smallFocusedMethods:·gc.alloc.rate thrpt 30 1404.228 ± 8.831 MB/sec
smallFocusedMethods:·gc.alloc.rate.norm thrpt 30 270.320 ± 0.001 B/op
smallFocusedMethods:·gc.churn.G1_Eden_Space thrpt 30 1393.473 ± 10.493 MB/sec
smallFocusedMethods:·gc.churn.G1_Eden_Space.norm thrpt 30 268.250 ± 1.193 B/op
smallFocusedMethods:·gc.churn.G1_Old_Gen thrpt 30 0.186 ± 0.001 MB/sec
smallFocusedMethods:·gc.churn.G1_Old_Gen.norm thrpt 30 0.036 ± 0.001 B/op
smallFocusedMethods:·gc.count thrpt 30 1986.000 counts
smallFocusedMethods:·gc.time thrpt 30 1011.000 ms
基本上通用的指令是“相互化”的
结论
我并不是说这个例子具有代表性,但它似乎证明了几点:
- 使用较小的方法可以提高代码的可读性
- 较小的方法通常是内联的,因此您很可能不需要支付额外方法调用的费用(它将与性能无关)
- 如上面的示例所示,在某些情况下,使用较小的方法可能会改进全局内联
最后一点:如果您的代码中有一部分对性能非常关键,而这些考虑因素很重要,那么您应该检查JIT输出,以微调代码,重要的是在前后对其进行概要分析。我已经阅读了许多文章,其中指出较小的方法(以将方法表示为Java字节码所需的字节数衡量)在编译热方法(运行最频繁的方法)时,JIT(即时编译器)更有可能进行内联它们描述了方法内联如何为生成的机器代码提供更好的性能。简言之:较小的方法为JIT提供了更多的选择,在识别热方法时如何将字节码编译成机器代码,这允许更复杂的优化 为了测试这一理论,我创建了一个JMH类,其中包含两个基准方法,每个方法都包含相同的行为,但考虑的因素不同。第一个基准被命名为
monolithicMethod
(所有代码都在一个方法中),第二个基准被命名为smallFocusedMethods
,并且经过重构,每个主要行为都被移入了自己的方法中。smallFocusedMethods
基准如下所示:
0x0000000002780624: int3 ;*invokevirtual m
; - javaapplication27.TestInline::main@20 (line 10)
0x00000000026d0121: add ecx,edi ;*iinc
; - javaapplication27.TestInline::m2@7 (line 33)
; - javaapplication27.TestInline::m@7 (line 24)
; - javaapplication27.TestInline::main@20 (line 10)
@Benchmark
public void smallFocusedMethods(TestState state) {
int i = state.value;
if (i < 90) {
actionOne(i, state);
} else {
actionTwo(i, state);
}
}
private void actionOne(int i, TestState state) {
state.sb.append(Integer.toString(i)).append(
": has triggered the first type of action.");
int result = i;
for (int j = 0; j < i; ++j) {
result += j;
}
state.sb.append("Calculation gives result ").append(Integer.toString(
result));
}
private void actionTwo(int i, TestState state) {
state.sb.append(i).append(" has triggered the second type of action.");
int result = i;
for (int j = 0; j < 3; ++j) {
for (int k = 0; k < 3; ++k) {
result *= k * j + i;
}
}
state.sb.append("Calculation gives result ").append(Integer.toString(
result));
}
Benchmark Mode Cnt Score Error Units
monolithicMethod thrpt 30 7609784.687 ± 118863.736 ops/s
monolithicMethod:·gc.alloc.rate thrpt 30 1368.296 ± 15.834 MB/sec
monolithicMethod:·gc.alloc.rate.norm thrpt 30 270.328 ± 0.016 B/op
monolithicMethod:·gc.churn.G1_Eden_Space thrpt 30 1357.303 ± 16.951 MB/sec
monolithicMethod:·gc.churn.G1_Eden_Space.norm thrpt 30 268.156 ± 1.264 B/op
monolithicMethod:·gc.churn.G1_Old_Gen thrpt 30 0.186 ± 0.001 MB/sec
monolithicMethod:·gc.churn.G1_Old_Gen.norm thrpt 30 0.037 ± 0.001 B/op
monolithicMethod:·gc.count thrpt 30 2123.000 counts
monolithicMethod:·gc.time thrpt 30 1060.000 ms
smallFocusedMethods thrpt 30 7855677.144 ± 48987.206 ops/s
smallFocusedMethods:·gc.alloc.rate thrpt 30 1404.228 ± 8.831 MB/sec
smallFocusedMethods:·gc.alloc.rate.norm thrpt 30 270.320 ± 0.001 B/op
smallFocusedMethods:·gc.churn.G1_Eden_Space thrpt 30 1393.473 ± 10.493 MB/sec
smallFocusedMethods:·gc.churn.G1_Eden_Space.norm thrpt 30 268.250 ± 1.193 B/op
smallFocusedMethods:·gc.churn.G1_Old_Gen thrpt 30 0.186 ± 0.001 MB/sec
smallFocusedMethods:·gc.churn.G1_Old_Gen.norm thrpt 30 0.036 ± 0.001 B/op
smallFocusedMethods:·gc.count thrpt 30 1986.000 counts
smallFocusedMethods:·gc.time thrpt 30 1011.000 ms
简而言之,这些数字表明smallFocusedMethods
方法运行速度快了3.2%,差异具有统计学意义(置信度为99.9%)。请注意,内存使用率(基于垃圾收集分析)没有显著差异。因此,您可以在不增加开销的情况下获得更快的性能
我已经运行了各种类似的基准测试,以测试小型、集中的方法是否能提供更好的吞吐量,并且我发现在我尝试过的所有情况下,吞吐量的提高都在3%到7%之间。但实际的提高很可能很大程度上取决于所使用的JVM版本,以及在if/else块中执行的分布(为了夸大第一个“动作”的热度,我在第一个和第二个动作中分别使用了90%和10%,但我看到了吞吐量的提高,即使在if/else块链中的分布更为均匀),以及每个可能动作所做工作的实际复杂性。因此,请务必