为什么JMH说返回1比返回0更快?

23

有人能解释一下为什么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

对于1、2和0都表现出相同的行为

# 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

嘿,我正在创建基准并观察到了这种行为,不是因为我花时间测量这个例子。这只是这个主题的简化版本。 - Artur Mkrtchyan
然后创建一个非简化版本,展示你所看到的。它需要足够复杂,以至于所有热点工作都与你想要测量的内容无关。 - Thorbjørn Ravn Andersen
1个回答

35

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

当返回0、1或任何其他整数时,速度上显然没有区别。然而,这取决于JMH如何消耗该值以及HotSpot JIT如何对其进行编译。

为了防止JIT优化掉计算,JMH使用特殊的Blackhole类来消耗从基准测试返回的值。 这里是一个整数值的示例:

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

也就是说,Blackhole.consume() 执行了更多的 CPU 指令,导致 return 0 的速度变慢。

致JMH开发人员的说明:我建议对 Blackhole.consume 进行改写,如下所示:

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

volatile long l1 = Long.MIN_VALUE 时,条件仍然是永远为假,但它将被编译为所有返回值相等。


1
@AlekseyShipilev 一定会对此感兴趣 :) - apangin
这解释了很多。谢谢! - Artur Mkrtchyan
1
@apangin:这是一个有趣的想法,但是:a)它不能扩展到其他数据类型,并且我们希望在消耗的类型中保持一致;b)它适用于扩展转换,这对于32位平台(如ARM)来说是不好的。 - Aleksey Shipilev
13
这个例子的真正意义在于,纳秒级别的基准测试需要在汇编级别进行验证,使用JMH的 -prof perfasm 功能可以很方便地实现 :) - Aleksey Shipilev
@apangin:如果JMH不完美,那么哪个是完美的呢? - Gaurav

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接