@@ -171,3 +171,64 @@ the case in Xamarin.Android -- then the overhead isn't actually that bad,
171171on a per-method invoke basis. It appears to only get really bad when
172172invoking the same method repetitively, * a lot* , which I believe shouldn't
173173be * that* common a use case (outside of image manipulation?).
174+
175+ ### JNI and P/Invoke
176+
177+ [ Commit 9d2dfc5] [ 9d2dfc5 ] observed that there's a fair bit of overhead
178+ associated with using ` SafeHandle ` s, in large parge because ` SafeHandle ` s
179+ need to be [ * thread safe* ] [ cbrumme-SafeHandle ] in order to prevent
180+ [ handle recycling attacks] [ handle-recycle ] . Xamarin.Android doesn't
181+ suffer from handle recycling attacks * only* because Mono's SGEN GC
182+ conservatively scans the stack, prolonging the lifetime of all temporaries
183+ found there. If/when Xamarin.Android moves to a precise GC for the stack,
184+ this will no longer be the case and handle recycling attacks -- along
185+ with possibly finalizing/` Dispose() ` ing of instances
186+ * while they're still being used* -- can become "a thing".
187+
188+ [ 9d2dfc5 ] : https://github.com/xamarin/Java.Interop/commit/9d2dfc5
189+ [ cbrumme-SafeHandle ] : http://blogs.msdn.com/b/cbrumme/archive/2004/02/20/77460.aspx
190+ [ handle-recycle ] : http://blogs.msdn.com/b/cbrumme/archive/2003/04/19/51365.aspx
191+
192+ An idea that came to mind to reduce the overhead of ` SafeHandle ` use was to
193+ P/Invoke to a native library to perform the ` JNIEnv ` function pointer
194+ invocations instead of using ` delegate ` invocations alongside
195+ ` Marshal.GetDelegateForFunctionPointer() ` , as is currently the case.
196+
197+ This was implemented in the [ pinvoke-jnienv] [ pinvoke-jnienv ] branch,
198+ in [ commit 802842a3] [ 802842a3 ] .
199+
200+ [ pinvoke-jnienv ] : https://github.com/xamarin/Java.Interop/commits/pinvoke-jnienv
201+ [ 802842a3 ] : https://github.com/xamarin/Java.Interop/commit/802842a361380812e290fe3585fea8c0a7a19b97
202+
203+ The result: Using P/Invoke * increases* invocation overhead:
204+
205+ # "Full" Invocations: JNIEnv::CallObjectMethod() + JNIEnv::DeleteLocalRef() for 10000 iterations
206+ Java.Interop Object.toString() Timing: 00:00:30.0774575; 3.00774575 ms/iteration -- ~386.391119190154x
207+ Xamarin.Android Object.toString() Timing: 00:00:00.0778420; 0.0077842 ms/iteration
208+ # JNIEnv::CallObjectMethod() for 500 iterations
209+ Java.Interop Object.toString() Timing: 00:00:00.8041310; 1.608262 ms/CallVirtualObjectMethod() -- ~266.648207712969x
210+ Xamarin.Android CallObjectMethod() Timing: 00:00:00.0030157; 0.0060314 ms/CallObjectMethod()
211+ # JNIEnv::DeleteLocalRef() for 500 iterations
212+ Java.Interop JniLocalReference.Dispose() Timing: 00:00:00.8979645; 1.795929 ms/Dispose() -- ~1661.05160932297x
213+ Xamarin.Android DeleteLocalRef() Timing: 00:00:00.0005406; 0.0010812 ms/DeleteLocalRef()
214+ ## Breaking down the above Object.toString() + JniLocalReference.Dispose() timings, the JNI calls:
215+ # JNIEnv::CallObjectMethod: SafeHandle vs. IntPtr
216+ Java.Interop safeCall() Timing: 00:00:00.0058775; 0.011755 ms/SafeHandle JNIEnv::CallObjectMethodA() -- ~2.14538618776464x
217+ Java.Interop P/Invoke safeCall() Timing: 00:00:00.0069479; 0.0138958 ms/SafeHandle JNIEnv::CallObjectMethodA() -- ~2.53610016060739x
218+ Java.Interop unsafeCall() Timing: 00:00:00.0027396; 0.0054792 ms/IntPtr JNIEnv::CallObjectMethodA()
219+ # JNIEnv::DeleteLocalRef: SafeHandle vs. IntPtr
220+ Java.Interop safeDel() Timing: 00:00:00.0006010; 0.001202 ms/SafeHandle JNIEnv::DeleteLocalRef() -- ~1.47412312975227x
221+ Java.Interop P/Invoke safeDel() Timing: 00:00:00.0007480; 0.001496 ms/SafeHandle JNIEnv::DeleteLocalRef() -- ~1.83468236448369x
222+ Java.Interop unsafeDel() Timing: 00:00:00.0004077; 0.0008154 ms/IntPtr JNIEnv::DeleteLocalRef
223+
224+ In particular, note the ` Java.Interop P/Invoke ` lines:
225+
226+ Java.Interop P/Invoke safeCall() Timing: 00:00:00.0069479; 0.0138958 ms/SafeHandle JNIEnv::CallObjectMethodA() -- ~2.53610016060739x
227+ Java.Interop P/Invoke safeDel() Timing: 00:00:00.0007480; 0.001496 ms/SafeHandle JNIEnv::DeleteLocalRef() -- ~1.83468236448369x
228+
229+ Compare to the ` SafeHandle ` -using delegate-based invocations:
230+
231+ Java.Interop safeCall() Timing: 00:00:00.0058775; 0.011755 ms/SafeHandle JNIEnv::CallObjectMethodA() -- ~2.14538618776464x
232+ Java.Interop safeDel() Timing: 00:00:00.0006010; 0.001202 ms/SafeHandle JNIEnv::DeleteLocalRef() -- ~1.47412312975227x
233+
234+ Surprisingly, using delegates results in less overhead than using P/Invoke.
0 commit comments