Repository navigation
NativeAOT TODO list #9784
Description
Activity
- addedArea: App+Library BuildIssues when building Library projects or Application projects.Issues when building Library projects or Application projects.
on Feb 12, 2025 - addedneeds-triageIssues that need to be assigned.Issues that need to be assigned.
on Feb 12, 2025 - removedneeds-triageIssues that need to be assigned.Issues that need to be assigned.
on Feb 12, 2025 Alternate "MVP 1.1" typemap idea, for posterity: instead of a
Dictionary<string, Type>, have two fields:Dictionary<string, RuntimeTypeHandle>orstring[]+RuntimeTypeHandle[]pair, plusDictionary<string, Type>
The linker step would populate (1), which might be faster because
RuntimeTypeHandleis a stuct.At runtime, consult (2) first, and if a type mapping doesn't exist, use (1) to lookup the
RuntimeTypeHandle+Type.GetTypeFromHandle(RuntimeTypeHandle), caching the result in (2).Questions:
- Can we lookup the RuntimeTypeHandle value from Cecil?
- The caching will require locking. What kind of overhead will this imply?
There must be a way to use a
TypeHandlewith Mono.Cecil, as the IL "typemap MVP 1.0" is writing is doing this:
android/src/Microsoft.Android.Sdk.ILLink/TypeMappingStep.cs
Lines 70 to 83 in 70bd636
/* * IL_0000: ldarg.0 * IL_0001: ldfld class [System.Runtime]System.Collections.Generic.IDictionary`2<string, class [System.Runtime]System.Type> Microsoft.Android.Runtime.NativeAotTypeManager::TypeMappings * IL_0006: ldstr "java/lang/Object" * IL_000b: ldtoken [Mono.Android]Java.Lang.Object * IL_0010: call class [System.Runtime]System.Type [System.Runtime]System.Type::GetTypeFromHandle(valuetype [System.Runtime]System.RuntimeTypeHandle) * IL_0015: callvirt instance void class [System.Runtime]System.Collections.Generic.IDictionary`2<string, class [System.Runtime]System.Type>::Add(!0, !1) */ il.Emit (Mono.Cecil.Cil.OpCodes.Ldarg_0); il.Emit (Mono.Cecil.Cil.OpCodes.Ldfld, field); il.Emit (Mono.Cecil.Cil.OpCodes.Ldstr, javaKey); il.Emit (Mono.Cecil.Cil.OpCodes.Ldtoken, module.ImportReference (typeDefinition)); il.Emit (Mono.Cecil.Cil.OpCodes.Call, getTypeFromHandle); il.Emit (Mono.Cecil.Cil.OpCodes.Callvirt, addMethod); LdtokenreturnsRuntimeTypeHandleso you can use that in the dictionary. It's, however, not a pattern that can be represented in C# and ILSpy will not decode it correctly.I'm currently exploring this direction for the type map: https://gist.github.com/simonrozsival/8b3f2294d7fa1cf84ca32e8145984c98
- there's no startup initialization - the Java class name hashes are stored in a sorted array that's baked into the assembly
- the runtime type handles are stored in code and are accessed via a jumptable (switch) - this allows us to "serialize" RuntimeTypeHandles in a way which Native AOT understands
What do you think?
@simonrozsival brought up a point that the
RuntimeTypeHandlevalues might not match up at runtime in NativeAOT:- We run ILLink
- At some point during/after ILLink, we generate a managed typemap, if we used
RuntimeTypeHandle... - ILC runs and uses its own set of integers (who knows?) instead of
RuntimeTypeHandle
ILC runs and uses its own set of integers (who knows?) instead of RuntimeTypeHandle
I don't think I expressed myself correctly. If we generate IL that builds the
Dictionary<string, RuntimeTypeHandle>usingil.Emit (Mono.Cecil.Cil.OpCodes.Ldtoken, module.ImportReference (typeDefinition));, then Native AOT will compile this correctly. That's not the problem.I was trying to say that we cannot serialize the raw numeric values of
RuntimeTypeHandlewhile we're processing the IL and use it for lookups viaType.GetTypeFromHandleat runtime.We can of course create an array of RTH, but it will need to be initialized at startup:
static readonly RuntimeTypeHandle[] s_handles = [ typeof(X).TypeHandle, typeof(Y).TypeHandle ]; // is compiled to IL like this: .method private hidebysig specialname rtspecialname static void .cctor () cil managed { // Method begins at RVA 0x2058 // Code size 56 (0x38) .maxstack 8 IL_0000: ldc.i4.2 IL_0001: newarr [System.Runtime]System.RuntimeTypeHandle IL_0006: dup IL_0007: ldc.i4.0 IL_0008: ldtoken X IL_000d: call class [System.Runtime]System.Type [System.Runtime]System.Type::GetTypeFromHandle(valuetype [System.Runtime]System.RuntimeTypeHandle) IL_0012: callvirt instance valuetype [System.Runtime]System.RuntimeTypeHandle [System.Runtime]System.Type::get_TypeHandle() IL_0017: stelem [System.Runtime]System.RuntimeTypeHandle IL_001c: dup IL_001d: ldc.i4.1 IL_001e: ldtoken Y IL_0023: call class [System.Runtime]System.Type [System.Runtime]System.Type::GetTypeFromHandle(valuetype [System.Runtime]System.RuntimeTypeHandle) IL_0028: callvirt instance valuetype [System.Runtime]System.RuntimeTypeHandle [System.Runtime]System.Type::get_TypeHandle() IL_002d: stelem [System.Runtime]System.RuntimeTypeHandle IL_0032: stsfld valuetype [System.Runtime]System.RuntimeTypeHandle[] X::s_handles IL_0037: ret } // end of method X::.cctor
We can of course create an array of RTH, but it will need to be initialized at startup:
ILC can interpret some static constructors at compile time and produce preinitialized data in the executable image. It would not work for the Dictionary but it can probably work for a simple array... (or can be tweaked to work if it doesn't at the moment)
Reacted by Šimon RozsívalFor zero-alloc string->Type dictionary known at build-time, we can generate a lookup method using stack-based
ReadOnlySpanand collection expressions like https://dotnetfiddle.net/1P6yNL (replaceseedDataDictionarywith your data). Similarly, for string->string https://dotnetfiddle.net/wFNdTh,string ->int https://dotnetfiddle.net/54v6CF and so on.Benchmark string->Type
using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Diagnosers; using BenchmarkDotNet.Running; BenchmarkRunner.Run<LookupBenchmark>(); [MemoryDiagnoser] // Memory diagnostics enabled public class LookupBenchmark { private readonly string[] existingKeys = [nameof(Int32), nameof(Object), nameof(String), nameof(Boolean)]; private readonly string[] missingKeys = ["key999", "unknown", "hello", "test123"]; [Benchmark] public void Dictionary_PositiveLookups() { Dictionary<string, Type> dataDictionary = new() { [nameof(Int32)] = typeof(int), [nameof(Object)] = typeof(object), [nameof(String)] = typeof(string), [nameof(Boolean)] = typeof(bool) }; foreach (var key in existingKeys) { var result = dataDictionary[key]; } } [Benchmark] public void Dictionary_NegativeLookups() { Dictionary<string, Type> dataDictionary = new() { [nameof(Int32)] = typeof(int), [nameof(Object)] = typeof(object), [nameof(String)] = typeof(string), [nameof(Boolean)] = typeof(bool) }; foreach (var key in missingKeys) { _ = dataDictionary.TryGetValue(key, out _); } } [Benchmark] public void Static_PositiveLookups() { foreach (var key in existingKeys) { _ = TryFind(key, out _); } static bool TryFind(ReadOnlySpan<char> key, out Type value) { if (key.Length == 0) { value = default; return false; } ReadOnlySpan<char> keysConcat = "Int32ObjectStringBoolean"; ReadOnlySpan<int> keyOffsets = [0, 5, 11, 17]; ReadOnlySpan<int> keyLengths = [5, 6, 6, 7]; ReadOnlySpan<Type> values = [typeof(System.Int32), typeof(System.Object), typeof(System.String), typeof(System.Boolean)]; int searchStart = 0; while (true) { int foundOffset = searchStart + keysConcat[searchStart..].IndexOf(key); if (foundOffset < 0) { value = default; return false; } int keyIndex = keyOffsets.IndexOf(foundOffset); if (key.Length != keyLengths[keyIndex]) { if (keyIndex + 1 >= keyOffsets.Length) { value = default; return false; } searchStart = keyOffsets[keyIndex + 1]; continue; } value = values[keyIndex]; return true; } } } [Benchmark] public void Static_NegativeLookups() { foreach (var key in missingKeys) { _ = TryFind(key, out _); } static bool TryFind(ReadOnlySpan<char> key, out Type value) { if (key.Length == 0) { value = default; return false; } ReadOnlySpan<char> keysConcat = "Int32ObjectStringBoolean"; ReadOnlySpan<int> keyOffsets = [0, 5, 11, 17]; ReadOnlySpan<int> keyLengths = [5, 6, 6, 7]; ReadOnlySpan<Type> values = [typeof(System.Int32), typeof(System.Object), typeof(System.String), typeof(System.Boolean)]; int searchStart = 0; while (true) { int foundOffset = searchStart + keysConcat[searchStart..].IndexOf(key); if (foundOffset < 0) { value = default; return false; } int keyIndex = keyOffsets.IndexOf(foundOffset); if (key.Length != keyLengths[keyIndex]) { if (keyIndex + 1 >= keyOffsets.Length) { value = default; return false; } searchStart = keyOffsets[keyIndex + 1]; continue; } value = values[keyIndex]; return true; } } } }
// * Summary * BenchmarkDotNet v0.14.0, macOS Sequoia 15.3.1 (24D70) [Darwin 24.3.0] Apple M1 Pro, 1 CPU, 10 logical and 10 physical cores .NET SDK 9.0.201 [Host] : .NET 8.0.10 (8.0.1024.46610), Arm64 RyuJIT AdvSIMD DefaultJob : .NET 8.0.10 (8.0.1024.46610), Arm64 RyuJIT AdvSIMDMethod Mean Error StdDev Gen0 Allocated Dictionary_PositiveLookups 148.37 ns 3.748 ns 10.873 ns 0.0739 464 B Dictionary_NegativeLookups 142.39 ns 3.373 ns 9.568 ns 0.0739 464 B Static_PositiveLookups 42.03 ns 0.837 ns 1.253 ns - - Static_NegativeLookups 28.27 ns 0.443 ns 0.393 ns - - With just array of 4 items, we start to see the difference.
Roslyn source generator: https://github.com/am11/GeneratedLookup
@am11 we have an implementation that doesn't use
Dictionaryat all now, so some of the comments above are out of date:Reacted by Adeel Mujahid- addedArea: NativeAOTIssues that only occur when using NativeAOT.Issues that only occur when using NativeAOT.
on May 8, 2025
What do we need to do for NativeAOT?
Current status for .NET 11 NativeAOT.
To investigate next:
dotnet publish; this was reported to fail whiledotnet buildworked during the original preview implementationdotnet new mauitemplate with NativeAOTList of TODOs:
dotnet new android && dotnet publish -p:PublishAot=trueMicrosoft.Android.Runtime.NativeAOT.dll#9760dotnet new mauirunsdebug.mono.log, including disablinglref/grefloggingmonodroid_log()for NativeAOT$(PublishAotUsingRuntimePack)=true$(PublishAotUsingRuntimePack)=true#9815SynchronizationContext#9883AppDomain.CurrentDomain.UnhandledExceptionis still explicitly skipped under NativeAOTTrimmerRootAssemblyNativeAOT size deoptimizationlibc++_shared.so; NativeAOT currently linkslibc++_static.alibc++dependency entirelyJNINativeWrapper.CreateDelegate(...)[RequiresDynamicCode], producing an AOT warning for callersJNINativeWrapper.CreateDelegate. #9741$(VerifyReferenceAotCompatibility)