Description
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
encodedMember
string, 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-16string
instance 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
byte
instead 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.)