首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >循环数组,索引和增强的for循环之间的性能差异

循环数组,索引和增强的for循环之间的性能差异
EN

Stack Overflow用户
提问于 2022-01-04 17:51:22
回答 1查看 646关注 0票数 5

JLS声明,对于数组,“增强型for语句等效于表单的basic语句”。但是,如果我检查为JDK8生成的字节码,就会生成两个变体不同的字节码,如果我试图测量性能,令人惊讶的是,增强的字节码似乎会提供更好的结果(在jdk8上).有人能告诉我为什么吗?我想是因为不正确的jmh测试,所以如果是那样的话,请建议如何修复它。(我知道JMH状态不测试使用循环,但我认为这不适用于这里,因为我实际上是试图在这里测量循环)

我的JMH测试相当简单(可能太简单了),但我无法解释结果。测试JMH代码如下,典型结果如下:

代码语言:javascript
复制
JdkBenchmarks.enhanced  avgt    5  2556.281 ±  31.789  ns/op
JdkBenchmarks.indexed   avgt    5  4032.164 ± 100.121  ns/op

这意味着通常增强的for循环更快,而且它的测量比索引循环更精确,因此我们无法解决测量不确定度的差异。对于由随机整数或较大数组初始化的数组,主要是相同的结果。

代码语言:javascript
复制
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();
    }

}
EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2022-01-05 19:26:21

JIT编译器不能相信values 不会在循环中改变,您正在观察会发生什么。此外,在如此微小的基准中, 成本占主导地位,掩盖了结果。

简化测试:

代码语言:javascript
复制
@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);
        }
    }
}

enhancedindexed_cached下运行-prof perfasm显示了这个热循环(我专门使用@CompilerControl(DONT_INLINE)@Benchmark方法单独编译,这使perfasm输出更容易理解):

代码语言:javascript
复制
         ↗  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显示:

代码语言:javascript
复制
         ↗  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):

代码语言:javascript
复制
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调用之前执行许多错误的代码!):

代码语言:javascript
复制
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),它将重新显化,因为生成的代码将更加混乱:

代码语言:javascript
复制
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 enhanced
票数 8
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/70583053

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档