我正在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然后从final字段声明中删除value并重新编译基准测试:
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()的成本保持不变,那么为什么字段访问变得如此昂贵呢?
发布于 2022-04-04 21:54:39
我假设在基准测试方法中调用String.valueOf()的成本保持不变
事实并非如此。在第一种情况下,方法参数是常数,因此JIT可以应用常传播优化。
考虑一个非常简单的例子:
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更复杂,但是当使用常量调用时,它仍然可以跳过一些比较和算术操作。
https://stackoverflow.com/questions/71736845
复制相似问题