I'm comparing performance of MethodHandle::invoke
and direct static method invokation. Here is the static method:
public class IntSum {
public static int sum(int a, int b){
return a + b;
}
}
And here is my benchmark:
@State(Scope.Benchmark)
public class MyBenchmark {
public int first;
public int second;
public final MethodHandle mhh;
@Benchmark
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
public int directMethodCall() {
return IntSum.sum(first, second);
}
@Benchmark
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
public int finalMethodHandle() throws Throwable {
return (int) mhh.invoke(first, second);
}
public MyBenchmark() {
MethodHandle mhhh = null;
try {
mhhh = MethodHandles.lookup().findStatic(IntSum.class, "sum", MethodType.methodType(int.class, int.class, int.class));
} catch (NoSuchMethodException | IllegalAccessException e) {
e.printStackTrace();
}
mhh = mhhh;
}
@Setup
public void setup() throws Exception {
first = 9857893;
second = 893274;
}
}
I got the following result:
Benchmark Mode Cnt Score Error Units
MyBenchmark.directMethodCall avgt 5 3.069 ± 0.077 ns/op
MyBenchmark.finalMethodHandle avgt 5 6.234 ± 0.150 ns/op
MethodHandle
has some performance degradation.
Running it with -prof perfasm
shows this:
....[Hottest Regions]...............................................................................
31.21% 31.98% C2, level 4 java.lang.invoke.LambdaForm$DMH::invokeStatic_II_I, version 490 (27 bytes)
26.57% 28.02% C2, level 4 org.sample.generated.MyBenchmark_finalMethodHandle_jmhTest::finalMethodHandle_avgt_jmhStub, version 514 (84 bytes)
20.98% 28.15% C2, level 4 org.openjdk.jmh.infra.Blackhole::consume, version 497 (44 bytes)
As far as I could figure out the reason for the benchmark result is that the Hottest Region 2 org.sample.generated.MyBenchmark_finalMethodHandle_jmhTest::finalMethodHandle_avgt_jmhStub
contains all the type-checks performed by the MethodHandle::invoke
inside the JHM loop. Assembly output fragment (some code ommitted):
....[Hottest Region 2]..............................................................................
C2, level 4, org.sample.generated.MyBenchmark_finalMethodHandle_jmhTest::finalMethodHandle_avgt_jmhStub, version 519 (84 bytes)
;...
0x00007fa2112119b0: mov 0x60(%rsp),%r10
;...
0x00007fa2112119d4: mov 0x14(%r12,%r11,8),%r8d ;*getfield form
0x00007fa2112119d9: mov 0x1c(%r12,%r8,8),%r10d ;*getfield customized
0x00007fa2112119de: test %r10d,%r10d
0x00007fa2112119e1: je 0x7fa211211a65 ;*ifnonnull
0x00007fa2112119e7: lea (%r12,%r11,8),%rsi
0x00007fa2112119eb: callq 0x7fa211046020 ;*invokevirtual invokeBasic
;...
0x00007fa211211a01: movzbl 0x94(%r10),%r10d ;*getfield isDone
;...
0x00007fa211211a13: test %r10d,%r10d
;jumping at the begging of jmh loop if not done
0x00007fa211211a16: je 0x7fa2112119b0 ;*aload_1
;...
Before calling the invokeBasic
we perform the type-checking (inside the jmh loop) which affects the output avgt.
QUESTION: Why isn't all the type-check moved outside of the loop? I declared public final MethodHandle mhh;
inside the benchmark. So I expected the compiler can figured it out and eliminate the same type-checks. How to make the same typechecks eliminated? Is it possible?
You use reflective invocation of MethodHandle
. It works roughly like Method.invoke
, but with less run-time checks and without boxing/unboxing. Since this MethodHandle
is not static final
, JVM does not treat it as constant, that is, MethodHandle's target is a black box and cannot be inlined.
Even though mhh
is final, it contains instance fields like MethodType type
and LambdaForm form
that are reloaded on each iteration. These loads are not hoisted out of the loop because of a black-box call inside (see above). Furthermore, LambdaForm
of a MethodHandle
can be changed (customized) in run-time between calls, so it needs to be reloaded.
Use static final
MethodHandle. JIT will know the target of such MethodHandle and thus may inline it at the call site.
Even if you have non-static MethodHandle, you may bind it to a static CallSite and invoke it as fast as direct methods. This is similar to how lambdas are called.
private static final MutableCallSite callSite = new MutableCallSite(
MethodType.methodType(int.class, int.class, int.class));
private static final MethodHandle invoker = callSite.dynamicInvoker();
public MethodHandle mh;
public MyBenchmark() {
mh = ...;
callSite.setTarget(mh);
}
@Benchmark
public int boundMethodHandle() throws Throwable {
return (int) invoker.invokeExact(first, second);
}
invokeinterface
instead of MethodHandle.invoke
as @Holger suggested. An instance of interface for calling given MethodHandle can be generated with LambdaMetafactory.metafactory()
.Make MethodHandle mhh
static:
Benchmark Mode Samples Score Error Units
directMethodCall avgt 5 0,942 ± 0,095 ns/op
finalMethodHandle avgt 5 0,906 ± 0,078 ns/op
Non-static:
Benchmark Mode Samples Score Error Units
directMethodCall avgt 5 0,897 ± 0,059 ns/op
finalMethodHandle avgt 5 4,041 ± 0,463 ns/op
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With