-
Notifications
You must be signed in to change notification settings - Fork 556
[NativeAOT] Generate optimized type mapping #9856
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[NativeAOT] Generate optimized type mapping #9856
Conversation
src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/TypeMapping.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/TypeMapping.cs
Outdated
Show resolved
Hide resolved
410116e
to
99a21e7
Compare
Co-authored-by: Aaron Robinson <[email protected]> Co-authored-by: Jan Kotas <[email protected]>
@jonathanpeppers, @pjcollins: any idea why
|
We automatically try to pull in .pdb files for all
|
src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/TypeMapping.cs
Outdated
Show resolved
Hide resolved
{ | ||
ulong hash = Hash (javaClassName); | ||
|
||
// the hashes array is sorted and all the hashes are unique |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you going to check the uniqueness of the hashcodes at build time to guarantee this?
An alternative way to deal with this would be to allow non-unique hashcodes and check all candidates in the map. If you do that, you can change the hashcodes to 32-bit that makes the map 2x smaller and dealing with hashcodes faster, at the cost of a rare hash collision.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you going to check the uniqueness of the hashcodes at build time to guarantee this?
Yes, the check is here: https://github.com/dotnet/android/pull/9856/files#diff-8e22a99ebc58c9417a48013888fc90d72fa6b638aff5d2d449cbf2c78bd0a8fbR149-R161
If you do that, you can change the hashcodes to 32-bit that makes the map 2x smaller and dealing with hashcodes faster, at the cost of a rare hash collision.
That's definitely worth trying. Assuming even the XxHash32 collisions are very rare, in the typical case there should be just 1 string comparison to verify that the hash belongs to the expected result.
…AOT/TypeMapping.cs Co-authored-by: Jan Kotas <[email protected]>
Should hopefully fix the build.
Context: 70bd636b04ebf9dba36c632cdec586452e98cb8a
Context: f48b97cb084aa8331592290cb0b581af0f794bcc
Replaces the current managed `Dictionary<string, Type>` type mapping
dictionary introduced in 70bd636b, which needs to be fully build at
app startup by repeatedly calling `Add()`, with a pre-generated
static array of class name hashes. The hashes are sorted so that we
can binary search to find the index corresponding to a Java class name.
This roughly mirrors what is done in `libxamarin-app.so` (f48b97cb).
Once we know the index of a Java class class, we can use a generated
IL switch to look up the corresponding `Type`. I chose to generate
this jumptable in code because this is what NativeAOT understands
best. Constructing a static array of metadata tokens is AFAIK not an
option for NativeAOT.
The generated IL is conceptually equivalent to the following C# code:
static partial class TypeMapping
{
internal static bool TryGetType(string javaClassName, [NotNullWhen (true)] out Type? type)
{
var hash = XxHash3.HashToUInt64(javaClassName);
var index = BinarySearch(JavaClassNameHashes, hash);
if (index < 0) {
type = null;
return false;
}
type = GetTypeByIndex(index);
// verify that this is not just a hash collision…
return true;
}
private static Type? GetTypeByIndex(int index) => index switch {
0 => typeof (A),
1 => typeof (B),
// …
_ => null,
};
private static HashesArray s_hashes = …; // rva static field with initial value
private static ReadOnlySpan<ulong> JavaClassNameHashes => new ReadOnlySpan<ulong>(ref s_hashes, 54);
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 432)]
private struct HashesArray
{
}
}
The `TypeMapping.s_hashes` field is stored as a byte buffer in the
assembly and it is loaded from the static RVA field of `TypeMapping`.
The relevant generated IL looks like this for a simple app such as
`samples/NativeAOT.csproj`:
.class private auto ansi beforefieldinit Microsoft.Android.Runtime.TypeMapping
extends [System.Private.CoreLib]System.Object
{
.method private hidebysig static
class [System.Private.CoreLib]System.Type
GetTypeByIndex (int32 index) cil managed
{
IL_0000: ldarg.0
IL_0001: switch (IL_00e3, IL_00ee, …, IL_032a)
IL_00de: br IL_0335
// index 0
IL_00e3: ldtoken [Mono.Android]Java.IO.File
IL_00e8: call class [System.Private.CoreLib]System.Type [System.Private.CoreLib]System.Type::GetTypeFromHandle(valuetype [System.Private.CoreLib]System.RuntimeTypeHandle)
IL_00ed: ret
// index 1
IL_00ee: ldtoken [Mono.Android]Android.Runtime.InputStreamAdapter
IL_00f3: call class [System.Private.CoreLib]System.Type [System.Private.CoreLib]System.Type::GetTypeFromHandle(valuetype [System.Private.CoreLib]System.RuntimeTypeHandle)
IL_00f8: ret
// …
// index N
IL_032a: ldtoken [Mono.Android]Java.Lang.StackTraceElement
IL_032f: call class [System.Private.CoreLib]System.Type [System.Private.CoreLib]System.Type::GetTypeFromHandle(valuetype [System.Private.CoreLib]System.RuntimeTypeHandle)
IL_0334: ret
IL_0335: ldnull
IL_0336: ret
} // end of method TypeMapping::GetTypeByIndex
.class nested private explicit ansi HashesArray
extends [System.Private.CoreLib]System.ValueType
{
.pack 1
.size 432
} // end of class HashesArray
.field assembly static initonly valuetype Microsoft.Android.Runtime.TypeMapping/HashesArray s_hashes at I_00004A33
.data cil I_00004A33 = bytearray (
c5 27 01 ad 86 95 90 00 2e b5 ff b6 eb ef 2c 04 // .'............,.
5d 20 71 9d c0 8f 78 09 24 db 2e 9c f9 cc 8d 0a // ] q...x.$.......
bc d1 4e 4c de f1 d9 0b 83 c5 37 e8 d7 c3 b3 0d // ..NL......7.....
f9 b1 64 fe 72 d0 d1 0f 8e 99 32 40 ab c2 dd 10 // ..d.r.....2@....
95 3d a9 89 b0 a9 81 12 da a1 1a e9 62 96 ed 1e // .=..........b...
7c 52 0b 4c 25 28 37 26 6c 89 44 40 86 46 7b 2a // |R.L%(7&[email protected]{*
a5 c1 d6 0b 52 8e 84 37 e2 97 e3 1f 8e fa b2 47 // ....R..7.......G
2a 9a c3 8b 8e d0 d8 47 37 b2 3a d6 7d 11 9e 49 // *......G7.:.}..I
34 29 c1 56 77 21 7b 54 8e 7a 2a 5e 62 1c 2d 5b // 4).Vw!{T.z*^b.-[
a0 a4 53 91 42 b7 18 60 51 0d 00 bc 15 cc 4e 60 // ..S.B..`Q.....N`
e7 00 00 75 1c cc 4b 68 fb 35 55 50 bf bc e7 68 // ...u..Kh.5UP...h
ee 7c 46 a9 1c c5 e8 68 e7 3c 4e 6d 2f b4 b7 6c // .|F....h.<Nm/..l
d4 1c 3d 47 2d 2a c0 6c 67 31 a1 c1 cb 5c c2 70 // ..=G-*.lg1...\.p
66 1f cb d6 59 50 76 73 8e 50 c2 47 46 19 8f 76 // f...YPvs.P.GF..v
0c a3 d9 c9 c1 49 bb 76 47 49 52 5a 0c 86 c7 76 // .....I.vGIRZ...v
43 06 de 1a 9c 34 a4 77 07 5a 17 8d 74 58 2c 7c // C....4.w.Z..tX,|
d0 05 a6 c5 b5 ff 97 7c f1 36 3a fc 31 bc 8f 86 // .......|.6:.1...
35 45 f1 6e 96 88 2a 87 60 b1 04 f9 af c9 ee 89 // 5E.n..*.`.......
12 08 0d 44 6c a0 32 8c 35 3a b0 71 3c 2c 44 91 // ...Dl.2.5:.q<,D.
c8 25 9d d5 9e 5d 1b 93 09 94 5f 54 87 b1 96 a0 // .%...]...._T....
59 32 69 4d 3f c4 a6 a0 5f 7d 08 b5 e6 2a 9c a8 // Y2iM?..._}...*..
ce 20 91 5b 2a c5 8c af 5f 58 9d 1e 18 3f 65 bf // . .[*..._X...?e.
57 90 af e1 71 13 35 c1 4b b8 2d 6e 9c 25 7a c5 // W...q.5.K.-n.%z.
68 e8 b8 1d 2d 1d 94 d1 5b dc 7a b6 52 f4 cf d4 // h...-...[.z.R...
56 48 36 d6 2b 3f 18 d7 d0 46 bc 99 55 15 bc da // VH6.+?...F..U...
c2 5c aa 9f 40 3b 87 ed 68 5c 3c ea 95 3a f2 ef // .\..@;..h\<..:..
b3 c5 64 33 43 f6 0c f1 dc e9 8b df 19 2a 72 f9 // ..d3C........*r.
)
}
~~ Notes ~~
I'm using xxhash64 from the System.IO.Hashing NuGet package. This
algorithm is used because it's what we're successfully using in the
native type maps. Unfortunately, it presented a challenge when used
in a custom linker step, as I needed to manually load the assembly
into an ALC, because ILLink would not load the package dependency of
our custom step assembly automatically.
The switch could become too big for the AOT compiler/RyuJIT to compile.
This might need to be revisited later and split into several separate
methods; see the [macios managed static registrar][0] for a similar
approach used in the macios managed static registrar.
I'm not very happy with reverse lookups (`Type` to Java class name).
The `NativeAotTypeManager.GetSimpleReferences()` method *can* return
multiple values. It's not clear to me when this can happen (invokers?)
and if the inverse type mapping can also be just 1:1. If that were the
case, the related code could be revisited and optimized. I added
caching for the time being because this method is repeatedly called
with the same inputs at the startup of MAUI apps.
[0]: https://github.com/dotnet/macios/blob/f14b02010f1e1b59547f609caab35a40f61f5869/docs/managed-static-registrar.md#method-mapping |
This PR replaces the current managed type mapping dictionary, which needs to be fully build at startup by individually calling
Add
methods, with a pre-generated static array of class name hashes.The array of hashes is sorted so we can use binary search to find the index corresponding to any class name. This idea is not new, it is exacatly how the native type maps work at the moment.
Once we know the index of a class, we can use a generated IL switch to look up the corresponding
Type
. I chose to generate this jumptable in code because this is what Native AOT understands best. Constructing a static array of metadata tokens is AFAIK not an option for Native AOT.The generated IL is conceptually equivalent to the following C# code:
The
s_hashes
field is stored as a byte buffer in the assembly and it is loaded from the static RVA field ofTypeMapping
. The relevant generated code looks like this for a simple app (samples/NativeAOT.csproj
):Notes
I'm not very happy with the inverse lookups (Type -> java class name). TheTypeManager.GetSimpleReferences
method CAN return multiple values. It's not clear to me when this can happen (invokers?) and if the inverse type mapping can also be just 1:1. If that were the case, the related code could be revisited and optimized. I at least added caching for the time being because this method is repeatedly called with the same inputs at the startup of MAUI apps./cc @ivanpovazan @vitek-karas @AaronRobinsonMSFT