Proposal: Spans for JNI Member lookups
Currently, Java.Interop.JniPeerMembers member lookups are backed via dictionary lookup with string as the key-type, e.g.
https://github.com/xamarin/java.interop/blob/7dc270dbb83948b278bee38fc83bf9ae5cd42a7e/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticMethods.cs#L18 https://github.com/xamarin/java.interop/blob/7dc270dbb83948b278bee38fc83bf9ae5cd42a7e/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticMethods.cs#L25-L36
This works, and provides a reasonably "user-friendly" interface, but has hidden performance implications:
JniPeerMembers.GetNameAndSignature()allocates strings, which creates "garbage" objects, as they're not long-lived: https://github.com/xamarin/java.interop/blob/7dc270dbb83948b278bee38fc83bf9ae5cd42a7e/src/Java.Interop/Java.Interop/JniPeerMembers.cs#L183-L188- The dictionary lookup requires traversing the entire
encodedMemberstring, which is a (usually fast!) O(n) operation. - There is "implicit" marshaling overhead, as e.g.
JniType.GetStaticMethod()involves a P/Invoke, which marshals a UTF-16stringinstance to a UTF-8 native string: https://github.com/xamarin/java.interop/blob/7dc270dbb83948b278bee38fc83bf9ae5cd42a7e/src/Java.Interop/Java.Interop/JniType.cs#L252-L257
(Note: this still appears to contribute ~2ms to Xamarin.Android app startup, so it's not a huge source of performance implications, but it is A Thing™ to consider…)
These methods are usually invoked via generator-emitted code, such as:
namespace Android.App {
public partial class Activity {
public static unsafe long InstanceCount {
[Register ("getInstanceCount", "()J", "")]
get {
const string __id = "getInstanceCount.()J";
try {
var __rm = _members.StaticMethods.InvokeInt64Method (__id, null);
return __rm;
} finally {
}
}
}
}
}
We could improve this by:
- Pre-compute a hashcode value.
- Use
Span<T>types, which - Use
byteinstead ofchar, and - Contain embedded nulls
Thus, instead of:
const string __id = "getInstanceCount.()J";
var __rm = _members.StaticMethods.InvokeInt64Method (__id, null);
We would instead have generator emit:
static readonly int hash_getInstanceCount = JniPeerMembers.ComputeHash ("getInstanceCount.()J");
…
ReadOnlySpan<byte> __id = new stackalloc {
(byte) 'g',
(byte) 'e',
…
(byte) 't',
(byte) 0, // terminates `getInstanceCount`
(byte) '.'
(byte) '(',
(byte) ')',
(byte) 'J',
(byte) 0, // terminates `()J`
};
var __rm = _members.StaticMethods.InvokeInt64Method (hash_getInstanceCount, __id, null);
JniEnvironment.StaticMethods.GetStaticMethodID() & co. could then be overloaded to take ReadOnlySpan<T> parameters (as overloads to the current string parameters).
TODO: verify that ReadOnlySpan<byte> as a P/Invoke parameter (1) works, and (2) passes the data as-is, no copy or marshaling involved.
Doing all this would remove the need to marshal anything: everything would already be UTF-8, because that's what generator's stackalloc would contain, so there's no System.String-to-const char* marshaling involved (as is currently required). This would also help with the embedded Dictionary<string, JniMethodInfo> lookup, which could now become a Dictionary<int, JniMethodInfo> lookup (as the hash is pre-computed).
On the downside, this would be a kind-of ABI break: binding assemblies using these new members couldn't be used on older Xamarin.Android SDKs/etc. (This is where multi-targeting solves everything.)
Apparently this works in C# 11:
byte[] array = "hello"; // new byte[] { 0x68, 0x65, 0x6c, 0x6c, 0x6f }
Span<byte> span = "dog"; // new byte[] { 0x64, 0x6f, 0x67 }
ReadOnlySpan<byte> span = "cat"; // new byte[] { 0x63, 0x61, 0x74 }
So maybe we can avoid the stackalloc syntax?
https://devblogs.microsoft.com/dotnet/csharp-11-preview-updates/#utf-8-string-literals