Here I found the following code that shows the difference in perfomance for MethodHandles and Reflection:
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(3)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
public class MHOpto {
private int value = 42;
private static final Field static_reflective;
private static final MethodHandle static_unreflect;
private static final MethodHandle static_mh;
private static Field reflective;
private static MethodHandle unreflect;
private static MethodHandle mh;
// We would normally use @Setup, but we need to initialize "static final" fields here...
static {
try {
reflective = MHOpto.class.getDeclaredField("value");
unreflect = MethodHandles.lookup().unreflectGetter(reflective);
mh = MethodHandles.lookup().findGetter(MHOpto.class, "value", int.class);
static_reflective = reflective;
static_unreflect = unreflect; //LINE X!!!
static_mh = mh;
} catch (IllegalAccessException | NoSuchFieldException e) {
throw new IllegalStateException(e);
}
}
@Benchmark
public int plain() {
return value;
}
@Benchmark
public int dynamic_reflect() throws InvocationTargetException, IllegalAccessException {
return (int) reflective.get(this);
}
@Benchmark
public int dynamic_unreflect_invoke() throws Throwable {
return (int) unreflect.invoke(this);
}
@Benchmark
public int dynamic_unreflect_invokeExact() throws Throwable {
return (int) unreflect.invokeExact(this);
}
@Benchmark
public int dynamic_mh_invoke() throws Throwable {
return (int) mh.invoke(this);
}
@Benchmark
public int dynamic_mh_invokeExact() throws Throwable {
return (int) mh.invokeExact(this);
}
@Benchmark
public int static_reflect() throws InvocationTargetException, IllegalAccessException {
return (int) static_reflective.get(this);
}
@Benchmark
public int static_unreflect_invoke() throws Throwable {
return (int) static_unreflect.invoke(this);
}
@Benchmark
public int static_unreflect_invokeExact() throws Throwable {
return (int) static_unreflect.invokeExact(this);
}
@Benchmark
public int static_mh_invoke() throws Throwable {
return (int) static_mh.invoke(this);
}
@Benchmark
public int static_mh_invokeExact() throws Throwable {
return (int) static_mh.invokeExact(this);
}
}
And these are the results:
Benchmark Mode Cnt Score Error Units
MHOpto.dynamic_mh_invoke avgt 25 4.393 ± 0.003 ns/op
MHOpto.dynamic_mh_invokeExact avgt 25 4.394 ± 0.007 ns/op
MHOpto.dynamic_reflect avgt 25 5.230 ± 0.020 ns/op
MHOpto.dynamic_unreflect_invoke avgt 25 4.404 ± 0.023 ns/op
MHOpto.dynamic_unreflect_invokeExact avgt 25 4.397 ± 0.014 ns/op
MHOpto.plain avgt 25 1.858 ± 0.002 ns/op
MHOpto.static_mh_invoke avgt 25 1.862 ± 0.015 ns/op
MHOpto.static_mh_invokeExact avgt 25 1.859 ± 0.002 ns/op
MHOpto.static_reflect avgt 25 4.274 ± 0.011 ns/op
MHOpto.static_unreflect_invoke avgt 25 1.859 ± 0.002 ns/op
MHOpto.static_unreflect_invokeExact avgt 25 1.858 ± 0.002 ns/op
What I don't understand is this line of code:
static_unreflect = unreflect;
Is static_unreflect
(final) not equal to unreflect
(not final)? Then why do they show different results in perfomance? Could anyone explain?
Only the static final
variant of the MethodHandle is seen as a constant by the JIT, see e.g. ciField:
// Is this field a constant?
//
// Clarification: A field is considered constant if:
// 1. The field is both static and final
// 2. The field is not one of the special static/final
// non-constant fields. These are java.lang.System.in
// and java.lang.System.out. Abomination.
//
// A field is also considered constant if
// - it is marked @Stable and is non-null (or non-zero, if a primitive) or
// - it is trusted or
// - it is the target field of a CallSite object.
//
// See ciField::initialize_from() for more details.
//
// A user should also check the field value (constant_value().is_valid()), since
// constant fields of non-initialized classes don't have values yet.
bool is_constant() const { return _is_constant; }
And only calls through MethodHandles that are constant are inlined, see CallGenerator::for_method_handle_inline
Where it does several checks to see that the receiver is constant like:
Node* receiver = kit.argument(0);
if (receiver->Opcode() == Op_ConP) {
...
} else {
print_inlining_failure(C, callee, jvms->depth() - 1, jvms->bci(),
"receiver not constant");
}
This difference makes it so that the call to the static final
MethodHandle can be inlined, and is therefore roughly as fast as the plain case.
If you print out inlining information you can see this as well. e.g. you could add something like:
@Fork(jvmArgsAppend="-Xlog:inlining*=trace:inlining-%p-static_mh_invokeExact.txt")
To the benchmark methods.
In the static case you will see the call being inlined:
@ 17 org.sample.MyBenchmark::static_mh_invokeExact (8 bytes) force inline by CompileCommand
@ 4 java.lang.invoke.LambdaForm$MH/0x00000008000f0040::invokeExact_MT (23 bytes) force inline by annotation
@ 10 java.lang.invoke.Invokers::checkExactType (17 bytes) force inline by annotation
@ 1 java.lang.invoke.MethodHandle::type (5 bytes)
@ 14 java.lang.invoke.Invokers::checkCustomized (23 bytes) force inline by annotation
@ 1 java.lang.invoke.MethodHandleImpl::isCompileConstant (2 bytes)
@ 19 java.lang.invoke.LambdaForm$MH/0x00000008000f0440::getInt (34 bytes) force inline by annotation
@ 7 java.lang.invoke.DirectMethodHandle::fieldOffset (9 bytes) force inline by annotation
@ 12 java.lang.invoke.DirectMethodHandle::checkBase (5 bytes) force inline by annotation
@ 1 java.util.Objects::requireNonNull (14 bytes)
@ 8 java.lang.NullPointerException::<init> (5 bytes) don't inline Throwable constructors
@ 30 jdk.internal.misc.Unsafe::getInt (0 bytes) intrinsic
We're inlining all the way to the Unsafe::getInt
call (but the important part is that we see @ 19 java.lang.invoke.LambdaForm$MH/0x00000008000f0440::getInt
instead of the invokeBasic
).
In the dynamic case, you'll at most see:
@ 17 org.sample.MyBenchmark::dynamic_mh_invokeExact (8 bytes) force inline by CompileCommand
@ 4 java.lang.invoke.LambdaForm$MH/0x00000008000f0040::invokeExact_MT (23 bytes) force inline by annotation
@ 10 java.lang.invoke.Invokers::checkExactType (17 bytes) force inline by annotation
@ 1 java.lang.invoke.MethodHandle::type (5 bytes)
@ 12 java.lang.invoke.Invokers::newWrongMethodTypeException (36 bytes) callee is too large
@ 14 java.lang.invoke.Invokers::checkCustomized (23 bytes) force inline by annotation
@ 1 java.lang.invoke.MethodHandleImpl::isCompileConstant (2 bytes)
@ 19 java.lang.invoke.Invokers::maybeCustomize (28 bytes) don't inline by annotation
@ 19 java.lang.invoke.MethodHandle::invokeBasic(L)I (0 bytes) receiver not constant
I.e. in that case there is still an indirect call through the invokeBasic
stub, because "receiver not constant".