JMH 基准中的现场访问成本和不断折叠

Field access costs and constant folding in JMH benchmark

我是 运行 Java 17 上的以下基准:

@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Fork(jvmArgsAppend = {"-Xms2g", "-Xmx2g"})
public class ValueOfBenchmark {
    private final int value = 12345;

    @Benchmark
    public String concat() {
        return "" + value;
    }

    @Benchmark
    public String valueOf() {
        return String.valueOf(value);
    }
}

编译后我有这个字节码

  public concat()Ljava/lang/String;
  @Lorg/openjdk/jmh/annotations/Benchmark;()
   L0
    LINENUMBER 21 L0
    LDC "12345"
    ARETURN
   L1
    LOCALVARIABLE this Lcom/tsypanov/ovn/ValueOfBenchmark; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x1
  public valueOf()Ljava/lang/String;
  @Lorg/openjdk/jmh/annotations/Benchmark;()
   L0
    LINENUMBER 26 L0
    SIPUSH 12345
    INVOKESTATIC java/lang/String.valueOf (I)Ljava/lang/String;
    ARETURN
   L1
    LOCALVARIABLE this Lcom/tsypanov/ovn/ValueOfBenchmark; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

在这里我得出结论,javac 在编译时常量折叠访问字段的值。在 concat 方法中,它走得更远,预测了方法返回的最终值,但未能对 valueOf() 做同样的事情,结果 12345 的常量值被推到堆栈与随后调用 String.valueOf().

该基准测试给出了以下结果:

Benchmark                 Mode  Cnt  Score   Error  Units
ValueOfBenchmark.concat   avgt   40  1,665 ± 0,006  ns/op
ValueOfBenchmark.valueOf  avgt   40  4,475 ± 0,217  ns/op

然后我从 value 字段声明中删除 final 并重新编译基准:

public concat()Ljava/lang/String;
@Lorg/openjdk/jmh/annotations/Benchmark;()
 L0
  LINENUMBER 21 L0
  ALOAD 0
  GETFIELD com/tsypanov/ovn/ValueOfBenchmark.value : I
  INVOKEDYNAMIC makeConcatWithConstants(I)Ljava/lang/String; [
    // handle kind 0x6 : INVOKESTATIC
    java/lang/invoke/StringConcatFactory.makeConcatWithConstants(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
    // arguments:
    "\u0001"
  ]
  ARETURN
 L1
  LOCALVARIABLE this Lcom/tsypanov/ovn/ValueOfBenchmark; L0 L1 0
  MAXSTACK = 1
  MAXLOCALS = 1

// access flags 0x1
public valueOf()Ljava/lang/String;
@Lorg/openjdk/jmh/annotations/Benchmark;()
 L0
  LINENUMBER 26 L0
  ALOAD 0
  GETFIELD com/tsypanov/ovn/ValueOfBenchmark.value : I
  INVOKESTATIC java/lang/String.valueOf (I)Ljava/lang/String;
  ARETURN
 L1
  LOCALVARIABLE this Lcom/tsypanov/ovn/ValueOfBenchmark; L0 L1 0
  MAXSTACK = 1
  MAXLOCALS = 1

现在我们有invokedynamic for concat()方法,但是valueOf()基本保持不变,唯一的区别是从字段中读取int值。然而,这带来了显着的结果回归:

Benchmark                 Mode  Cnt   Score   Error  Units
ValueOfBenchmark.concat   avgt   40   9,829 ± 0,059  ns/op
ValueOfBenchmark.valueOf  avgt   40  10,238 ± 0,463  ns/op

我确实预料到了 concat() 方法,因为现在我们将两个值连接成一个字符串。

但是令我疑惑的是valueOf()方法的回归

我假设在基准方法中调用 String.valueOf() 的成本保持不变,那么为什么字段访问变得如此昂贵?

I assume that the costs of calling String.valueOf() within benchmark method remained the same

不是。在第一种情况下,方法参数是常量,因此 JIT 可以应用 Constant Propagation 优化。

考虑一个非常简单的例子:

static char getLastDigit(int n) {
    return (char) ((n % 10) + '0');
}

char c1 = getLastDigit(123);

char c2 = getLastDigit(this.n);

当用常量调用getLastDigit时,不需要执行实际的除法或加法——JIT可能会将整个调用替换为char c1 = '3';显然不能做同样的优化有一个变量。

当然,Integer.getChars更复杂,但是用常量调用时还是会跳过一些比较和算术运算。