You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[Java.Interp, jnienv-gen] Use P/Invoke for JNIEnv Method Invocation
(This analysis turned out to be a bust, which is why this is on a
branch, not master. This is for "historical purposes".)
Commit 9d2dfc5 attempted to dig deeper into the performance
characteristics of Java.Interop vs. Xamarin.Android, to see how bad it
compared for method invocation (Object.toString()) and JNI Local
Reference deletion.
Java.Interop...compared poorly.
"So," thinks I, "maybe part of the performance problem is due to
delegate invocations, and things will get better if we use P/Invoke?"
This isn't entirely crazy; see the JniMethodInvocationOverheadTiming
output, e.g. in commit c60f609:
Method Invoke: static void: JNI is 148x managed
C/JNI: 00:00:00.0240000
JNI: 00:00:00.0908659; 4x C/JNI
Managed: 00:00:00.0006153
Pinvoke: 00:00:00.0009919; 2x managed
The above suggests that a P/Invoke method invocation has invocation
overhead roughly 2x that of a non-inlined C# method -- i.e. quite
fast! -- whiel a delegate-based JNI invocation takes ~4x as long to
execute as the "same" JNI method invocation from C.
So surely if we replace all our delegate invocations with P/Invokes
that do the "same" thing we should be faster, right?
With that idea in mind, update jnienv-gen so that in addition to
spitting out a C# JniEnvironment definition of all supported JNI
methods, it removes the JniNativeInterfaceStruct declaration, all the
related delegate declarations, and replaces it with a bunch of
[DllImport]s to generated C code:
// JniEnvironment.g.cs:
[DllImport (JavaInteropLib, CallingConvention=CallingConvention.Cdecl)]
internal static extern void JavaInterop_DeleteLocalRef (JniEnvironmentSafeHandle env, IntPtr localRef);
internal static void DeleteLocalRef (IntPtr localRef)
{
JavaInterop_DeleteLocalRef (JniEnvironment.Current.SafeHandle, localRef);
}
/* JniEnvironment.g.c: */
void
JavaInterop_DeleteLocalRef (JNIEnv *env, void* localRef)
{
(*env)->DeleteLocalRef (env, localRef);
}
This involves adding a new required native library,
libJavaInterop.dylib/libJavaInterop.so, which contains the above
"forwarding P/Invoke targets" (the JavaInterop_* functions).
Put it all together, and what do we get?
# "Full" Invocations: JNIEnv::CallObjectMethod() + JNIEnv::DeleteLocalRef() for 10000 iterations
Java.Interop Object.toString() Timing: 00:00:30.0774575; 3.00774575 ms/iteration -- ~386.391119190154x
Xamarin.Android Object.toString() Timing: 00:00:00.0778420; 0.0077842 ms/iteration
# JNIEnv::CallObjectMethod() for 500 iterations
Java.Interop Object.toString() Timing: 00:00:00.8041310; 1.608262 ms/CallVirtualObjectMethod() -- ~266.648207712969x
Xamarin.Android CallObjectMethod() Timing: 00:00:00.0030157; 0.0060314 ms/CallObjectMethod()
# JNIEnv::DeleteLocalRef() for 500 iterations
Java.Interop JniLocalReference.Dispose() Timing: 00:00:00.8979645; 1.795929 ms/Dispose() -- ~1661.05160932297x
Xamarin.Android DeleteLocalRef() Timing: 00:00:00.0005406; 0.0010812 ms/DeleteLocalRef()
## Breaking down the above Object.toString() + JniLocalReference.Dispose() timings, the JNI calls:
# JNIEnv::CallObjectMethod: SafeHandle vs. IntPtr
Java.Interop safeCall() Timing: 00:00:00.0058775; 0.011755 ms/SafeHandle JNIEnv::CallObjectMethodA() -- ~2.14538618776464x
Java.Interop P/Invoke safeCall() Timing: 00:00:00.0069479; 0.0138958 ms/SafeHandle JNIEnv::CallObjectMethodA() -- ~2.53610016060739x
Java.Interop unsafeCall() Timing: 00:00:00.0027396; 0.0054792 ms/IntPtr JNIEnv::CallObjectMethodA()
# JNIEnv::DeleteLocalRef: SafeHandle vs. IntPtr
Java.Interop safeDel() Timing: 00:00:00.0006010; 0.001202 ms/SafeHandle JNIEnv::DeleteLocalRef() -- ~1.47412312975227x
Java.Interop P/Invoke safeDel() Timing: 00:00:00.0007480; 0.001496 ms/SafeHandle JNIEnv::DeleteLocalRef() -- ~1.83468236448369x
Java.Interop unsafeDel() Timing: 00:00:00.0004077; 0.0008154 ms/IntPtr JNIEnv::DeleteLocalRef
The lines of interest are the "P/Invoke safecall() Timing" and
"P/Invoke safeDel() timing", which provides the timings for invoking
the new P/Invoke wrappers, in between the previous delegate-based
timing and the timing we see if we bypass SafeHandles entirely.
The results are not wat I hoped for: P/Invoke was *slower* than the
previous delegate-based strategy!
# JNIEnv::CallObjectMethod: SafeHandle vs. IntPtr
Java.Interop safeCall() Timing: 00:00:00.0058775; 0.011755 ms/SafeHandle JNIEnv::CallObjectMethodA() -- ~2.14538618776464x
Java.Interop P/Invoke safeCall() Timing: 00:00:00.0069479; 0.0138958 ms/SafeHandle JNIEnv::CallObjectMethodA() -- ~2.53610016060739x
Java.Interop unsafeCall() Timing: 00:00:00.0027396; 0.0054792 ms/IntPtr JNIEnv::CallObjectMethodA()
# JNIEnv::DeleteLocalRef: SafeHandle vs. IntPtr
Java.Interop safeDel() Timing: 00:00:00.0006010; 0.001202 ms/SafeHandle JNIEnv::DeleteLocalRef() -- ~1.47412312975227x
Java.Interop P/Invoke safeDel() Timing: 00:00:00.0007480; 0.001496 ms/SafeHandle JNIEnv::DeleteLocalRef() -- ~1.83468236448369x
Java.Interop unsafeDel() Timing: 00:00:00.0004077; 0.0008154 ms/IntPtr JNIEnv::DeleteLocalRef
SafeHandles put a serious hurt on performance, and P/Invoke doesn't
help like I had hoped it would. :-(
0 commit comments