Post

How does Reflexion make non SF case faster

In my project Reflexion, non static final Method shows lower overhead than MethodHandle in this specific benchmark:

BenchmarkModeCntScoreErrorUnits
customCallavgt201.317±0.008ns/op
mhCallavgt204.510±0.081ns/op
Benchmark code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package io.github.lumine1909.reflexion;

import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.results.format.ResultFormatType;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import org.openjdk.jmh.runner.options.TimeValue;

import java.lang.invoke.MethodHandle;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

import static io.github.lumine1909.reflexion.internal.UnsafeUtil.IMPL_LOOKUP;

public class Benchmark {

  static class A {
    private String value = "42";

    public String call(int arg) {
      value = "42";
      return value;
    }
  }

  private static final A OBJECT = new A();

  private static Method reflect$call;

  private static MethodHandle mh$call;

  private static io.github.lumine1909.reflexion.Method<String> custom$call =
    io.github.lumine1909.reflexion.Method.of(A.class, "call", String.class, int.class);

  static {
    try {
      reflect$call = A.class.getDeclaredMethod("call", int.class);
      reflect$call.setAccessible(true);
      mh$call = IMPL_LOOKUP.unreflect(reflect$call);
    } catch (Exception e) {
      throw new ExceptionInInitializerError(e);
    }
  }

  public static void main(String[] args) throws RunnerException {
    new java.io.File("build/reports/jmh").mkdirs();

    Options options = new OptionsBuilder()
      .include(Benchmark.class.getName())
      .forks(1)
      .warmupIterations(10)
      .measurementIterations(20)
      .warmupTime(TimeValue.seconds(1))
      .measurementTime(TimeValue.seconds(1))
      .shouldDoGC(false)
      .threads(1)
      .mode(Mode.AverageTime)
      .timeUnit(TimeUnit.NANOSECONDS)
      .resultFormat(ResultFormatType.JSON)
      .result("build/reports/jmh/results.json")
      .build();

    new Runner(options).run();
  }

  @org.openjdk.jmh.annotations.Benchmark
  public String customCall() {
    return custom$call.invoke(OBJECT, 42);
  }

  @org.openjdk.jmh.annotations.Benchmark
  public String mhCall() throws Throwable {
    return (String) mh$call.invokeExact(OBJECT, 42);
  }
}

This blog is written to explain why this can happen.

Everything start here

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package io.github.lumine1909.reflexion.internal;

import java.lang.invoke.MethodHandle;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;

@SuppressWarnings("unchecked")
public final class MethodHolder {

    // Placeholders to exploit java inline
    private static final MethodHandle o0 = null, o1 = null, o2 = null, o3 = null, o4 = null, o5 = null, o6 = null, o7 = null, o8 = null, o9 = null, o10 = null, o11 = null, o12 = null, o13 = null, o14 = null, o15 = null, o16 = null, o17 = null, o18 = null, o19 = null, o20 = null, o21 = null, o22 = null, o23 = null, o24 = null, o25 = null, o26 = null, o27 = null, o28 = null, o29 = null, o30 = null, o31 = null, o32 = null, o33 = null, o34 = null, o35 = null, o36 = null, o37 = null, o38 = null, o39 = null, o40 = null, o41 = null, o42 = null, o43 = null, o44 = null, o45 = null, o46 = null, o47 = null, o48 = null, o49 = null, o50 = null, o51 = null, o52 = null, o53 = null, o54 = null, o55 = null, o56 = null, o57 = null, o58 = null, o59 = null, o60 = null, o61 = null, o62 = null, o63 = null;
    private static final Supplier<MethodHandle>[] OBJ = new Supplier[]{() -> o0, () -> o1, () -> o2, () -> o3, () -> o4, () -> o5, () -> o6, () -> o7, () -> o8, () -> o9, () -> o10, () -> o11, () -> o12, () -> o13, () -> o14, () -> o15, () -> o16, () -> o17, () -> o18, () -> o19, () -> o20, () -> o21, () -> o22, () -> o23, () -> o24, () -> o25, () -> o26, () -> o27, () -> o28, () -> o29, () -> o30, () -> o31, () -> o32, () -> o33, () -> o34, () -> o35, () -> o36, () -> o37, () -> o38, () -> o39, () -> o40, () -> o41, () -> o42, () -> o43, () -> o44, () -> o45, () -> o46, () -> o47, () -> o48, () -> o49, () -> o50, () -> o51, () -> o52, () -> o53, () -> o54, () -> o55, () -> o56, () -> o57, () -> o58, () -> o59, () -> o60, () -> o61, () -> o62, () -> o63};
    private static final AtomicInteger ID = new AtomicInteger();

    public static Supplier<MethodHandle> inline(MethodHandle handle) {
        int index = ID.getAndIncrement();
        try {
            UnsafeUtil.putObject(MethodHolder.class, "o" + index, handle);
            return OBJ[index];
        } catch (Throwable t) {
            return null;
        }
    }
}

This creates an inlineable lambda-based access path, and when a MethodHandle field is static final, JIT can often treat it as a stable value after inlining and escape analysis, enabling further optimization that fully eliminate MethodHandle overhead. And when call inline, it will replace an unused slot by the given MethodHandle.

Then how does lambda act as an accessor preserve the constantness?

Using javap -c -p -v MethodHolder.class, we can find something like this:

1
2
3
4
5
6
7
8
9
  private static java.lang.Object lambda$static$0();
    descriptor: ()Ljava/lang/Object;
    flags: (0x100a) ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=1, locals=0, args_size=0
         0: getstatic     #225                // Field o0:Ljava/lang/invoke/MethodHandle;
         3: areturn
      LineNumberTable:
        line 12: 0

Which looks like this in java code

1
2
3
private static /*synthetic*/ Object lambda$static$0() {
    return MethodHolder.o0;
}

And by adding VM option -Djdk.invoke.LambdaMetafactory.dumpProxyClassFiles, we can find the generated lambda class

1
2
3
4
5
6
7
8
9
10
11
12
13
package io.github.lumine1909.reflexion.internal;

import java.util.function.Supplier;

// $FF: synthetic class
final class MethodHolder$$Lambda implements Supplier {
    private MethodHolder$$Lambda() {
    }

    public Object get() {
        return MethodHolder.lambda$static$0();
    }
}

So when calling get, it will use the generated synthetic method to access the static final MethodHandle, so that the invocation becomes more amenable to inlining and JIT optimization.

These codes exploit static final fields combined with trivially inlined lambda accessors. As a result, the wrapper call chain is fully inlined and collapsed, leaving the MethodHandle invocation in a more optimized execution context with reduced surrounding overhead.

That’s the reason why in this benchmark, the custom layer outperforms MethodHandle invocation.

Note: this optimization only holds when the call site remains monomorphic and the JVM successfully inlines through the Supplier lambda chain. If the ID space becomes megamorphic or the lambda type becomes unstable, the performance advantage may disappear.

This post is licensed under CC BY 4.0 by the author.