grupo competicion java performance benchmarking jmh

java - competicion - ¿Por qué JMH dice que devolver 1 es más rápido que devolver 0



grupo jmh (1)

¿Alguien puede explicar por qué JMH dice que devolver 1 es más rápido que devolver 0?

Aquí está el código de referencia.

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; } }

Aquí está el resultado:

# 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

El mismo comportamiento para uno, dos y cero.

# 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


JMH es una buena herramienta pero aún no es perfecta.

Ciertamente, no hay diferencia de velocidad entre devolver 0, 1 o cualquier otro entero. Sin embargo, hace una gran diferencia en cómo JMH consume el valor y cómo lo compila HotSpot JIT.

Para evitar que JIT optimice los cálculos, JMH utiliza la clase Blackhole especial para consumir los valores devueltos desde un punto de referencia. Aquí hay uno para valores enteros:

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

Aquí i es un valor devuelto desde un punto de referencia. En su caso, es 0 o 1. Cuando i == 1 la condición de "no suceder" se parece a if (1 == i1 & 1 == i2) que se compila de la siguiente manera:

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

Pero cuando i == 0 JIT intenta "optimizar" dos comparaciones a 0 usando setne instrucciones de setne . Sin embargo, el código de resultado se vuelve demasiado complicado:

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

Es decir, el return 0 más lento se explica por más instrucciones de CPU ejecutadas en Blackhole.consume() .

Nota para los desarrolladores de JMH: sugeriría volver a escribir Blackhole.consume como

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

donde volatile long l1 = Long.MIN_VALUE . En este caso, la condición seguirá siendo siempre falsa, pero se compilará por igual para todos los valores de retorno.