Почему JMH говорит, что возврат 1 быстрее, чем возврат 0

Может кто-нибудь объяснить, почему 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

person Artur Mkrtchyan    schedule 22.07.2014    source источник
comment
Эй, я создавал базовый уровень и видел такое поведение, дело не в том, что я трачу свое время на измерение только этого примера. это упрощенная версия для этой темы.   -  person Artur Mkrtchyan    schedule 22.07.2014
comment
Затем создайте неупрощенную версию, показывающую то, что вы видите. Должен быть достаточно сложным, чтобы вся работа Hotspot была незначительной по отношению к тому, что вы хотите измерить.   -  person Thorbjørn Ravn Andersen    schedule 22.07.2014


Ответы (1)


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 пытается "оптимизировать" два сравнения с 0, используя setne инструкции. Однако код результата становится слишком сложным:

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().

Примечание для разработчиков JMH: я бы предложил переписать Blackhole.consume как

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

где volatile long l1 = Long.MIN_VALUE. В этом случае условие по-прежнему будет всегда-ложно, но оно будет скомпилировано одинаково для всех возвращаемых значений.

person apangin    schedule 22.07.2014
comment
@AlekseyShipilev это должно быть интересно :) - person apangin; 23.07.2014
comment
это многое объясняет. Благодарю вас! - person Artur Mkrtchyan; 23.07.2014
comment
@apangin: Это интересная идея, однако: а) она не масштабируется для других типов данных, и мы хотели бы сохранить согласованность потребления для потребляемых типов; б) это идет на расширение преобразования, что плохо для 32-битных платформ (например, ARM). - person Aleksey Shipilev; 23.07.2014
comment
Настоящий вывод из этого примера заключается в том, что нанотесты требуют проверки на уровне сборки, что теперь удобно с JMH -prof perfasm :) - person Aleksey Shipilev; 23.07.2014
comment
@apangin: если JMH не идеален, то какой? - person Gaurav; 11.04.2018