JLS声明,对于数组,“增强型for语句等效于表单的basic语句”。但是,如果我检查为JDK8生成的字节码,就会生成两个变体不同的字节码,如果我试图测量性能,令人惊讶的是,增强的字节码似乎会提供更好的结果(在jdk8上).有人能告诉我为什么吗?我想是因为不正确的jmh测试,所以如果是那样的话,请建议如何修复它。(我知道JMH状态不测试使用循环,但我认为这不适用于这里,因为我实际上是试图在这里测量循环)
我的JMH测试相当简单(可能太简单了),但我无法解释结果。测试JMH代码如下,典型结果如下:
JdkBenchmarks.enhanced avgt 5 2556.281 ± 31.789 ns/op
JdkBenchmarks.indexed avgt 5 4032.164 ± 100.121 ns/op这意味着通常增强的for循环更快,而且它的测量比索引循环更精确,因此我们无法解决测量不确定度的差异。对于由随机整数或较大数组初始化的数组,主要是相同的结果。
public class JdkBenchmarks {
@Benchmark
@BenchmarkMode(AverageTime)
@OutputTimeUnit(NANOSECONDS)
public void indexed(Blackhole blackhole, TestState testState) {
int length = testState.values.length;
for(int i = 0; i < length; i++) {
blackhole.consume(testState.values[i]);
}
}
@Benchmark
@BenchmarkMode(AverageTime)
@OutputTimeUnit(NANOSECONDS)
public void enhanced(Blackhole blackhole, TestState testState) {
for (int value : testState.values) {
blackhole.consume(value);
}
}
@State(Scope.Benchmark)
public static class TestState {
public int[] values;
@Setup
public void setupArray() {
int count = 1000;
values = new int[count];
for(int i = 0; i < count; i++) {
values[i] = i;
}
}
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JdkBenchmarks.class.getSimpleName())
.forks(1)
.build();
new Runner(opt).run();
}
}发布于 2022-01-05 19:26:21
JIT编译器不能相信values 不会在循环中改变,您正在观察会发生什么。此外,在如此微小的基准中, 成本占主导地位,掩盖了结果。
简化测试:
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(3)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
public class JdkBenchmarks {
public int[] values;
@Setup
public void setupArray() {
int count = 1000;
values = new int[count];
for(int i = 0; i < count; i++) {
values[i] = i;
}
}
@Benchmark
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
public void indexed(Blackhole bh) {
for(int i = 0; i < values.length; i++) {
bh.consume(values[i]);
}
}
@Benchmark
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
public void indexed_cached(Blackhole bh) {
int[] vs = values;
int length = vs.length;
for(int i = 0; i < length; i++) {
bh.consume(vs[i]);
}
}
@Benchmark
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
public void enhanced(Blackhole bh) {
for (int value : values) {
bh.consume(value);
}
}
}在enhanced和indexed_cached下运行-prof perfasm显示了这个热循环(我专门使用@CompilerControl(DONT_INLINE)让@Benchmark方法单独编译,这使perfasm输出更容易理解):
↗ 0x...4240: mov 0x10(%r8,%rsi,4),%r10d ; load values[i], blackhole it
22.68% │ 0x...4245: mov 0x14(%r8,%rsi,4),%r11d ; ... repeat 7 more times...
│ 0x...424a: mov 0x18(%r8,%rsi,4),%r10d ;
20.95% │ 0x...424f: mov 0x1c(%r8,%rsi,4),%r10d ;
0.02% │ 0x...4254: mov 0x20(%r8,%rsi,4),%r11d ;
24.73% │ 0x...4259: mov 0x24(%r8,%rsi,4),%r10d ;
0.24% │ 0x...425e: mov 0x28(%r8,%rsi,4),%r11d ;
20.04% │ 0x...4263: mov 0x2c(%r8,%rsi,4),%r10d ;
0.22% │ 0x...4268: add $0x8,%esi ; i += 8
│ 0x...426b: cmp %ebp,%esi ; compare i with length (in %ebp)
0.26% ╰ 0x...426d: jl 0x...4240 ; circle back if 8 more elements available很有效率!
使用indexed运行-prof perfasm显示:
↗ 0x...4170: mov 0xc(%r12,%r8,8),%r9d ; array bounds check, load values.length
3.42% │ 0x...4175: cmp %r9d,%r10d ; array bounds check, compare i
16.02% │ 0x...4178: jae 0x...41b1 ; failed? jump to exception handling
│ 0x...417a: lea (%r12,%r8,8),%r11 ; load values[i], part 1
0.04% │ 0x...417e: mov 0x10(%r11,%r10,4),%r11d ; load values[i], part 2
│ ; %r11d is blackholed
35.69% │ 0x...4183: mov 0xc(%rsi),%r8d ; get "values"
0.71% │ 0x...4187: mov 0x348(%r15),%r11 ; safepoint poll, part 1 (JVM infra)
4.03% │ 0x...418e: inc %r10d ; i++
0.12% │ 0x...4191: test %eax,(%r11) ; safepoint poll, part 2 (JVM infra)
27.74% │ 0x...4194: mov 0xc(%r12,%r8,8),%r9d ; load values.length
8.53% │ 0x...4199: cmp %r9d,%r10d ; check i < values.length
0.24% ╰ 0x...419c: jl 0x...4170 ; circle back if more 这是因为Blackhole.consume调用对编译器是不透明的(就像许多其他非内联调用一样),所以它必须保守地假定values可以在循环中间改变!
这意味着编译器不能将values存储在寄存器中,它不能信任数组边界检查始终成功,甚至不能保证循环终止(因此是safepoint轮询),而且最重要的是,循环展开不希望将每个元素的混乱加倍。
因此,您将得到这样的惩罚(TR 3970X,JDK17.0.2EA,Linux x86_64):
Benchmark Mode Cnt Score Error Units
JdkBenchmarks.enhanced avgt 5 144.962 ± 0.918 ns/op
JdkBenchmarks.indexed avgt 5 1030.981 ± 3.775 ns/op ; + 880 ns/op!
JdkBenchmarks.indexed_cached avgt 5 144.799 ± 0.643 ns/op ; same as enhanced附加乐趣部分:
在大多数JDK上,主要的成本是在此测试中调用Blackhole.consume的成本。与数组访问成本相比,Java风格的Blackhole成本相当低.使用JDK 17+和JMH-1.34,将使用Blackholes,从而为测试提供更多的保真度。
如果没有编译器黑洞,这种效果几乎完全隐藏在Blackhole开销中(>25x开销意味着我们可以在Blackhole调用之前执行许多错误的代码!):
Benchmark Mode Cnt Score Error Units
JdkBenchmarks.enhanced avgt 5 4062.866 ± 4.736 ns/op
JdkBenchmarks.indexed avgt 5 4071.620 ± 1.057 ns/op ; + 10 ns/op [whoops]
JdkBenchmarks.indexed_cached avgt 5 4061.390 ± 0.692 ns/op ; same as enhanced如果我们删除@CompilerControl(DONT_INLINE),它将重新显化,因为生成的代码将更加混乱:
Benchmark Mode Cnt Score Error Units
JdkBenchmarks.enhanced avgt 5 4067.118 ± 40.699 ns/op
JdkBenchmarks.indexed avgt 5 4601.370 ± 0.632 ns/op ; + 530 ns/op
JdkBenchmarks.indexed_cached avgt 5 4064.455 ± 1.554 ns/op ; same as enhancedhttps://stackoverflow.com/questions/70583053
复制相似问题