I'm running the following benchmark on 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);
}
}
After compilation I have this bytecode
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
Here I conclude that javac constant-folded the value of accessed field at compile time. In concat
method it went even further and predicted eventual value to be returned from the method, but failed to do the same for valueOf()
and as a result constant value of 12345
is pushed onto the stack with subsequent call to String.valueOf()
.
This benchmark gives the following results:
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
Then I remove final
from value
field declaration and recompile the benchmark:
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
Now we have invokedynamic
for concat()
method, but valueOf()
remained basically the same, the only difference is that int value is read from the field. This however brought significant regression of results:
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
For sure I expected this for concat()
method as now we do concat two values into one String.
But what puzzles me is the regression of valueOf()
method.
I assume that the costs of calling String.valueOf()
within benchmark method remained the same, so why does the field access become so expensive?
CodePudding user response:
I assume that the costs of calling String.valueOf() within benchmark method remained the same
It is not. In the first case, the method argument is constant, so JIT can apply the Constant Propagation optimization.
Consider a very simplified example:
static char getLastDigit(int n) {
return (char) ((n % 10) '0');
}
char c1 = getLastDigit(123);
char c2 = getLastDigit(this.n);
When getLastDigit
is called with a constant, there is no need to execute the actual division or addition - JIT may replace the entire call with char c1 = '3';
It obviously can't do the same optimization with a variable.
Of course, Integer.getChars
is more complex, but it still may skip some comparisons and arithmetic operations when called with a constant.