Understanding the performance of my FFI-based API

Maurizio Cimadamore maurizio.cimadamore at oracle.com
Tue Mar 14 11:43:33 UTC 2023


On 14/03/2023 11:06, Alan Paxton wrote:

> You might be able to point me at something that explains what goes on 
> under the cover of invocation, and why exact matters ? My overall 
> takeaway is that there are a number of rules of thumb for making use 
> of FFI fast, if you follow them you get equivalent performance to JNI, 
> with safety for free.

Hi Alan,

thanks for coming back to us - it’s great that you have been able to 
reproduce the results.

As for why “exact” matter - that has to do with the method handle 
machinery (upon which var handles are also built). Both 
VarHandle::get/set and MethodHandle::invokeXYZ are “polymorphic 
signature” methods. This means that the java static compiler will look 
at these calls, and emit a /specialized/ method descriptor for the call. 
So, if you have:

|String s = (String)methodHandle.invokeExact(10); |

The compiler will emit a descriptor for that call with type 
|(int)String|. (A similar thing happens for var handle calls).

Now, the “exact” bit has to do with whether the JVM can “trust” that the 
symbolic description emitted by javac is the same as the type of the 
invoked method handle. If that’s the case, we can go straight to the 
method handle, and most (all) of the method handle machinery goop 
disappears when the call is inlined.

If, however, we use an “inexact” call (e.g. MethodHandle::invoke), we 
are telling the JVM that it needs to be prepared to perform some 
adaptations - that is, the symbolic description might be different from 
the method handle type. For instance, our static descriptor might say:

|(int)String |

but our underlying method handle might be like this:

|(Integer)String |

Method handles allow these adaptation (boxing, and widening, mostly) by 
wrapping the target method handle in a so called “asType” adaptation. 
This returns a /new/ method handle which has a signature that matches 
that of the incoming arguments, converts them to the right values, and 
then forwards them to the original method handle.

So, there’s one extra level of indirection and, worse, since the same 
method handle can be adapted to many different signatures (e.g. both 
|(int)String| and |(Object)int| are valid adaptation for 
|(Integer)String|), the method handle maintains a “type cache”, so that 
if we keep adapting the same method handle to the same type we end up 
creating another adapter method handle (as we already have one). This 
explains why type adaptation typically ends up hurting performance quite 
a bit (even though the JVM have come a long way to deal with those in 
the last few years :-) ).

(Sidebar: the Java 19 FFM API, with its separation between 
MemorySegment, MemoryAddress and Addressable makes it really hard to 
write /exact/ method handle invocations, as the downcall method handle 
type says Addressable, while the user will probably just have a 
MemorySegment. This means that, in order to have javac write the correct 
symbolic description, users should cast their segment to Addressable, 
which seems counterintuitive. This was one of the main reasons as to why 
in 20 we have simplified the API to remove the split between 
MemorySegment and MemoryAddress).

Var handles work pretty much in the same way - except that, for var 
handles, we don’t have inexact and exact version of get/set (that would 
have resulted in way too many methods). Instead, the var handle will act 
as “exact” or “inexact” based on whether the symbolic description 
matches the var handle type. If you get the exact path, all is good, and 
no type adaptation is required. If you go the inexact path, then you go 
through some form of MethodHandle::asType, which ends up hurting 
performance.

Detecting performance issues due to lack of exact var handle invocation 
has been historically tricky. The “withInvokeExactBehavior” was added 
precisely to allow developers to “express their intentions” more clearly 
(kudos to Jorn!).

As for the need for |final|, the JVM can only truly apply maximum 
inlining of var handles and method handles if these are “true constants” 
- which, for the JVM this means /both/ static /and/ final.

Maurizio

​
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/panama-dev/attachments/20230314/12b26a01/attachment-0001.htm>


More information about the panama-dev mailing list