Search code examples
javaperformancejmh

Field access costs and constant folding in JMH benchmark


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?


Solution

  • 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.