【问题标题】:Why is JMH saying that returning 1 is faster than returning 0为什么JMH说返回1比返回0快
【发布时间】:2023-03-03 06:32:22
【问题描述】:

有人能解释一下为什么 JMH 说返回 1 比返回 0 快吗?

这是基准代码。

import org.openjdk.jmh.annotations.*;

import java.util.concurrent.TimeUnit;

@State(Scope.Thread)
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Fork(value = 3, jvmArgsAppend = {"-server", "-disablesystemassertions"})
public class ZeroVsOneBenchmark {

    @Benchmark
    @Warmup(iterations = 3, time = 2, timeUnit = TimeUnit.SECONDS)
    public int zero() {
        return 0;
    }

    @Benchmark
    @Warmup(iterations = 3, time = 2, timeUnit = TimeUnit.SECONDS)
    public int one() {
        return 1;
    }
}

结果如下:

# Run complete. Total time: 00:03:05

Benchmark                       Mode   Samples        Score  Score error    Units
c.m.ZeroVsOneBenchmark.one     thrpt        60  1680674.502    24113.014   ops/ms
c.m.ZeroVsOneBenchmark.zero    thrpt        60   735975.568    14779.380   ops/ms

一、二和零的行为相同

# Run complete. Total time: 01:01:56

Benchmark                       Mode   Samples        Score  Score error    Units
c.m.ZeroVsOneBenchmark.one     thrpt        90  1762956.470     7554.807   ops/ms
c.m.ZeroVsOneBenchmark.two     thrpt        90  1764642.299     9277.673   ops/ms
c.m.ZeroVsOneBenchmark.zero    thrpt        90   773010.467     5031.920   ops/ms

【问题讨论】:

  • 嘿,我正在创建基线并看到了这种行为,并不是我花时间来衡量这个例子。这是此线程的简化版本。
  • 然后创建一个非简化版本来显示您所看到的。需要足够复杂,以使所有热点工作与您要测量的内容无关。

标签: java performance benchmarking jmh


【解决方案1】:

JMH 是一个很好的工具,但仍然不完美。

当然,返回 0、1 或任何其他整数之间没有速度差异。但是,JMH 如何使用该值以及 HotSpot JIT 如何编译该值是不同的。

为了防止 JIT 优化计算,JMH 使用特殊的 Blackhole 类来使用从基准返回的值。 Here is 一个整数值:

public final void consume(int i) {
    if (i == i1 & i == i2) {
        // SHOULD NEVER HAPPEN
        nullBait.i1 = i; // implicit null pointer exception
    }
}

这里的i 是从基准测试返回的值。在您的情况下,它是 0 或 1。当 i == 1 永远不会发生的情况看起来像 if (1 == i1 & 1 == i2) 时编译如下:

0x0000000002b4ffe5: mov    0xb0(%r13),%r10d   ;*getfield i1
0x0000000002b4ffec: mov    0xb4(%r13),%r8d    ;*getfield i2
0x0000000002b4fff3: cmp    $0x1,%r8d
0x0000000002b4fff7: je     0x0000000002b50091  ;*return

但是当i == 0 JIT 尝试使用setne 指令“优化”与0 的两个比较时。但是结果代码变得太复杂了:

0x0000000002a40b28: mov    0xb0(%rdi),%r10d   ;*getfield i1
0x0000000002a40b2f: mov    0xb4(%rdi),%r8d    ;*getfield i2
0x0000000002a40b36: test   %r10d,%r10d
0x0000000002a40b39: setne  %r10b
0x0000000002a40b3d: movzbl %r10b,%r10d
0x0000000002a40b41: test   %r8d,%r8d
0x0000000002a40b44: setne  %r11b
0x0000000002a40b48: movzbl %r11b,%r11d
0x0000000002a40b4c: xor    $0x1,%r10d
0x0000000002a40b50: xor    $0x1,%r11d
0x0000000002a40b54: and    %r11d,%r10d
0x0000000002a40b57: test   %r10d,%r10d
0x0000000002a40b5a: jne    0x0000000002a40c15  ;*return

也就是说,较慢的return 0 可以通过在Blackhole.consume() 中执行的更多 CPU 指令来解释。

JMH 开发者注意事项:我建议重写Blackhole.consume 喜欢

if (i == l1) {
     // SHOULD NEVER HAPPEN
    nullBait.i1 = i; // implicit null pointer exception
}

在哪里volatile long l1 = Long.MIN_VALUE。在这种情况下,条件仍将是 always-false,但它将为所有返回值平等地编译。

【讨论】:

  • @AlekseyShipilev 一定对此感兴趣 :)
  • 这解释了很多。谢谢!
  • @apangin:这是一个有趣的想法,但是:a) 它不能扩展到其他数据类型,我们希望在使用的类型之间保持 consumer-s 的一致性; b) 它用于扩大转换,这对 32 位平台不利(想想 ARM)。
  • 从这个例子中真正的收获是,nanobenchmarks 需要在组装级别进行验证,现在使用 JMH 的 -prof perfasm 很方便 :)
  • @apangin:如果 JMH 不完美,那么哪个是完美的?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-07-05
  • 1970-01-01
  • 1970-01-01
  • 2015-06-21
  • 2021-07-12
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多