Skip to content

Commit 802842a

Browse files
committed
[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. :-(
1 parent 513f8e5 commit 802842a

File tree

9 files changed

+286
-292
lines changed

9 files changed

+286
-292
lines changed

Makefile

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
CONFIGURATION = Debug
22

33
DEPENDENCIES = \
4+
bin/$(CONFIGURATION)/libJavaInterop.dylib \
45
bin/$(CONFIGURATION)/libNativeTiming.dylib
56

67
TESTS = \
@@ -20,6 +21,14 @@ clean:
2021
xbuild /t:Clean
2122
rm -Rf bin/$(CONFIGURATION)
2223

24+
bin/$(CONFIGURATION)/libJavaInterop.dylib: src/Java.Interop/JniEnvironment.g.c
25+
gcc -g -shared -o $@ $< -m32 -I /System/Library/Frameworks/JavaVM.framework/Headers
26+
touch $@
27+
28+
src/Java.Interop/JniEnvironment.g.c: $(wildcard tools/jnienv-gen/*.cs)
29+
xbuild
30+
touch $@
31+
2332
bin/$(CONFIGURATION)/libNativeTiming.dylib: tests/NativeTiming/timing.c
2433
mkdir -p `dirname "$@"`
2534
gcc -g -shared -o $@ $< -m32 -I /System/Library/Frameworks/JavaVM.framework/Headers

src/Android.Interop/Android.Interop.csproj

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@
5757
<AndroidResource Include="Resources\values\Strings.xml" />
5858
</ItemGroup>
5959
<Import Project="$(MSBuildExtensionsPath)\Novell\Novell.MonoDroid.CSharp.targets" />
60+
<PropertyGroup>
61+
<BuildDependsOn>
62+
BuildNativeLibs;
63+
$(BuildDependsOn)
64+
</BuildDependsOn>
65+
</PropertyGroup>
6066
<ItemGroup>
6167
<ProjectReference Include="..\Java.Interop\Java.Interop.csproj">
6268
<Project>{94BD81F7-B06F-4295-9636-F8A3B6BDC762}</Project>
@@ -75,4 +81,13 @@
7581
<Link>java-interop.jar</Link>
7682
</AndroidEmbeddedJavaLibrary>
7783
</ItemGroup>
84+
<ItemGroup>
85+
<EmbeddedNativeLibrary Include="libs\armeabi\libJavaInterop.so" />
86+
<EmbeddedNativeLibrary Include="libs\armeabi-v7a\libJavaInterop.so" />
87+
<EmbeddedNativeLibrary Include="libs\x86\libJavaInterop.so" />
88+
</ItemGroup>
89+
<Target Name="BuildNativeLibs" DependsOnTargets="_ResolveMonoAndroidSdks" Inputs="..\Java.Interop\JniEnvironment.g.c;jni\Android.mk" Outputs="@(AndroidNativeLibrary)">
90+
<Error Text="Could not locate Android NDK." Condition="!Exists ('$(_AndroidNdkDirectory)\ndk-build')" />
91+
<Exec Command="&quot;$(_AndroidNdkDirectory)\ndk-build&quot;" />
92+
</Target>
7893
</Project>

src/Android.Interop/Tests/TestsSample.cs

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ namespace Android.InteropTests {
1818
[TestFixture]
1919
public class TestsSample : Java.InteropTests.JavaVMFixture {
2020

21+
const string JavaInteropLib = "JavaInterop";
22+
2123
const int Unified_ToString_Iterations = 10000;
2224
const int MaxLocalRefs = 500;
2325

@@ -30,6 +32,13 @@ public class TestsSample : Java.InteropTests.JavaVMFixture {
3032
static readonly int JNIEnvOffset_CallObjectMethodA = JNIEnvIndex_CallObjectMethodA * IntPtr.Size;
3133
static readonly int JNIEnvOffset_DeleteLocalRef = JNIEnvIndex_DeleteLocalRef * IntPtr.Size;
3234

35+
[DllImport (JavaInteropLib, CallingConvention = CallingConvention.Cdecl)]
36+
static extern JniLocalReference JavaInterop_CallObjectMethod (JniEnvironmentSafeHandle env, JniReferenceSafeHandle obj, JniInstanceMethodID method);
37+
38+
[DllImport (JavaInteropLib, CallingConvention = CallingConvention.Cdecl)]
39+
static extern void JavaInterop_DeleteLocalRef (JniEnvironmentSafeHandle env, IntPtr handle);
40+
41+
3342
TimeSpan GetXAMethodCallTiming ()
3443
{
3544
var k = JNIEnv.FindClass ("java/lang/Object");
@@ -95,7 +104,8 @@ void GetXACallObjectMethodAndDeleteLocalRefTimings (out TimeSpan callObjectMetho
95104
void GetJICallObjectMethodAndDeleteLocalRefTimings (
96105
out TimeSpan callVirtualObjectMethodTime, out TimeSpan disposeTime,
97106
out TimeSpan safeCallObjectMethodTime, out TimeSpan safeDeleteLocalRefTime,
98-
out TimeSpan unsafeCallObjectMethodTime, out TimeSpan unsafeDeleteLocalRefTime)
107+
out TimeSpan unsafeCallObjectMethodTime, out TimeSpan unsafeDeleteLocalRefTime,
108+
out TimeSpan pinvokeCallObjectMethodTime, out TimeSpan pinvokeDeleteLocalRefTime)
99109
{
100110

101111
using (var k = new JniType ("java/lang/Object"))
@@ -170,6 +180,21 @@ void GetJICallObjectMethodAndDeleteLocalRefTimings (
170180
}
171181
sw.Stop ();
172182
unsafeDeleteLocalRefTime = sw.Elapsed;
183+
184+
sw.Restart ();
185+
for (int i = 0; i < urs.Length; ++i) {
186+
rs [i] = JavaInterop_CallObjectMethod (sh, o, t);
187+
}
188+
sw.Stop ();
189+
pinvokeCallObjectMethodTime = sw.Elapsed;
190+
191+
sw.Restart ();
192+
for (int i = 0; i < rs.Length; ++i) {
193+
JavaInterop_DeleteLocalRef (sh, rs [i].DangerousGetHandle ());
194+
rs [i].SetHandleAsInvalid ();
195+
}
196+
sw.Stop ();
197+
pinvokeDeleteLocalRefTime = sw.Elapsed;
173198
}
174199
}
175200

@@ -183,7 +208,12 @@ public void CompareJniInvocationTiming ()
183208
GetXACallObjectMethodAndDeleteLocalRefTimings (out xaCallObjectMethodTime, out xaDeleteLocalRefTime);
184209

185210
TimeSpan callVirtualObjectMethodTiming, disposeTiming, safeCallTime, safeDelTime, unsafeCallTime, unsafeDelTime;
186-
GetJICallObjectMethodAndDeleteLocalRefTimings (out callVirtualObjectMethodTiming, out disposeTiming, out safeCallTime, out safeDelTime, out unsafeCallTime, out unsafeDelTime);
211+
TimeSpan pinvokeCallTime, pinvokeDelTime;
212+
GetJICallObjectMethodAndDeleteLocalRefTimings (
213+
out callVirtualObjectMethodTiming, out disposeTiming,
214+
out safeCallTime, out safeDelTime,
215+
out unsafeCallTime, out unsafeDelTime,
216+
out pinvokeCallTime, out pinvokeDelTime);
187217

188218
Console.WriteLine ("# \"Full\" Invocations: JNIEnv::CallObjectMethod() + JNIEnv::DeleteLocalRef() for {0} iterations", Unified_ToString_Iterations);
189219
Console.WriteLine (" Java.Interop Object.toString() Timing: {0}; {1,12} ms/iteration -- ~{2}x",
@@ -218,6 +248,10 @@ public void CompareJniInvocationTiming ()
218248
safeCallTime,
219249
safeCallTime.TotalMilliseconds / MaxLocalRefs,
220250
safeCallTime.TotalMilliseconds / unsafeCallTime.TotalMilliseconds);
251+
Console.WriteLine (" Java.Interop P/Invoke safeCall() Timing: {0}; {1,12} ms/SafeHandle JNIEnv::CallObjectMethodA() -- ~{2}x",
252+
pinvokeCallTime,
253+
pinvokeCallTime.TotalMilliseconds / MaxLocalRefs,
254+
pinvokeCallTime.TotalMilliseconds / unsafeCallTime.TotalMilliseconds);
221255
Console.WriteLine (" Java.Interop unsafeCall() Timing: {0}; {1,12} ms/IntPtr JNIEnv::CallObjectMethodA()",
222256
unsafeCallTime,
223257
unsafeCallTime.TotalMilliseconds / MaxLocalRefs);
@@ -226,6 +260,10 @@ public void CompareJniInvocationTiming ()
226260
safeDelTime,
227261
safeDelTime.TotalMilliseconds / MaxLocalRefs,
228262
safeDelTime.TotalMilliseconds / unsafeDelTime.TotalMilliseconds);
263+
Console.WriteLine (" Java.Interop P/Invoke safeDel() Timing: {0}; {1,12} ms/SafeHandle JNIEnv::DeleteLocalRef() -- ~{2}x",
264+
pinvokeDelTime,
265+
pinvokeDelTime.TotalMilliseconds / MaxLocalRefs,
266+
pinvokeDelTime.TotalMilliseconds / unsafeDelTime.TotalMilliseconds);
229267
Console.WriteLine (" Java.Interop unsafeDel() Timing: {0}; {1,12} ms/IntPtr JNIEnv::DeleteLocalRef",
230268
unsafeDelTime,
231269
unsafeDelTime.TotalMilliseconds / MaxLocalRefs);

src/Java.Interop/Java.Interop.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@
111111
<PropertyGroup>
112112
<Runtime Condition="'$(OS)' != 'Windows_NT'">mono</Runtime>
113113
</PropertyGroup>
114-
<Target Name="BuildJniEnvironment_g_cs" Inputs="$(OutputPath)\jnienv-gen.exe" Outputs="Java.Interop\JniEnvironment.g.cs">
114+
<Target Name="BuildJniEnvironment_g_cs" Inputs="$(OutputPath)\jnienv-gen.exe" Outputs="Java.Interop\JniEnvironment.g.cs;JniEnvironment.g.c">
115115
<Exec Command="$(Runtime) &quot;$(OutputPath)\jnienv-gen.exe&quot; Java.Interop\JniEnvironment.g.cs" />
116116
</Target>
117117
<ItemGroup>

src/Java.Interop/Java.Interop/JniEnvironment.cs

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,6 @@ protected override bool ReleaseHandle ()
2929
return false;
3030
}
3131

32-
internal unsafe JniEnvironmentInvoker CreateInvoker ()
33-
{
34-
IntPtr p = Marshal.ReadIntPtr (handle);
35-
return new JniEnvironmentInvoker ((JniNativeInterfaceStruct*) p);
36-
}
37-
3832
public override string ToString ()
3933
{
4034
return string.Format ("{0}(0x{1})", GetType ().FullName, handle.ToString ("x"));
@@ -58,7 +52,6 @@ internal JniEnvironment (JniEnvironmentSafeHandle safeHandle, JavaVM javaVM)
5852
{
5953
handle = safeHandle;
6054
vm = javaVM;
61-
Invoker = SafeHandle.CreateInvoker ();
6255

6356
previous = current;
6457
current = this;
@@ -82,8 +75,6 @@ static JniEnvironmentSafeHandle GetCurrentHandle (IntPtr jniEnvironmentHandle)
8275
return new JniEnvironmentSafeHandle (jniEnvironmentHandle);
8376
}
8477

85-
internal JniEnvironmentInvoker Invoker;
86-
8778
public JniEnvironmentSafeHandle SafeHandle {
8879
get {return handle ?? RootEnvironment.SafeHandle;}
8980
}
@@ -94,7 +85,7 @@ public JavaVM JavaVM {
9485
return vm;
9586

9687
JavaVMSafeHandle vmh;
97-
int r = Invoker.GetJavaVM (SafeHandle, out vmh);
88+
int r = Handles.JavaInterop_GetJavaVM (SafeHandle, out vmh);
9889
if (r < 0)
9990
throw new InvalidOperationException ("JNIEnv::GetJavaVM() returned: " + r);
10091

src/Java.Interop/Java.Interop/JniInstanceMethodID.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,12 @@ public bool CallVirtualBooleanMethod (JniReferenceSafeHandle @this, params JValu
4141

4242
public sbyte CallVirtualSByteMethod (JniReferenceSafeHandle @this)
4343
{
44-
return JniEnvironment.Members.CallSByteMethod (@this, this);
44+
return JniEnvironment.Members.CallByteMethod (@this, this);
4545
}
4646

4747
public sbyte CallVirtualSByteMethod (JniReferenceSafeHandle @this, params JValue[] parameters)
4848
{
49-
return JniEnvironment.Members.CallSByteMethod (@this, this, parameters);
49+
return JniEnvironment.Members.CallByteMethod (@this, this, parameters);
5050
}
5151

5252
public char CallVirtualCharMethod (JniReferenceSafeHandle @this)
@@ -141,12 +141,12 @@ public bool CallNonvirtualBooleanMethod (JniReferenceSafeHandle @this, JniRefere
141141

142142
public sbyte CallNonvirtualSByteMethod (JniReferenceSafeHandle @this, JniReferenceSafeHandle declaringType)
143143
{
144-
return JniEnvironment.Members.CallNonvirtualSByteMethod (@this, declaringType, this);
144+
return JniEnvironment.Members.CallNonvirtualByteMethod (@this, declaringType, this);
145145
}
146146

147147
public sbyte CallNonvirtualSByteMethod (JniReferenceSafeHandle @this, JniReferenceSafeHandle declaringType, params JValue[] parameters)
148148
{
149-
return JniEnvironment.Members.CallNonvirtualSByteMethod (@this, declaringType, this, parameters);
149+
return JniEnvironment.Members.CallNonvirtualByteMethod (@this, declaringType, this, parameters);
150150
}
151151

152152
public char CallNonvirtualCharMethod (JniReferenceSafeHandle @this, JniReferenceSafeHandle declaringType)

src/Java.Interop/Java.Interop/JniStaticMethodID.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,12 @@ public bool CallBooleanMethod (JniReferenceSafeHandle type, params JValue[] para
4242

4343
public sbyte CallSByteMethod (JniReferenceSafeHandle type)
4444
{
45-
return JniEnvironment.Members.CallStaticSByteMethod (type, this);
45+
return JniEnvironment.Members.CallStaticByteMethod (type, this);
4646
}
4747

4848
public sbyte CallSByteMethod (JniReferenceSafeHandle type, params JValue[] parameters)
4949
{
50-
return JniEnvironment.Members.CallStaticSByteMethod (type, this, parameters);
50+
return JniEnvironment.Members.CallStaticByteMethod (type, this, parameters);
5151
}
5252

5353
public char CallCharMethod (JniReferenceSafeHandle type)

0 commit comments

Comments
 (0)