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
更复杂,但是用常量调用时还是会跳过一些比较和算术运算。
我是 运行 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
更复杂,但是用常量调用时还是会跳过一些比较和算术运算。