Skip to content

NativeAOT TODO list #9784

Description

@jonathanpeppers

What do we need to do for NativeAOT?

Current status for .NET 11 NativeAOT.

To investigate next:

  • Re-test dotnet publish; this was reported to fail while dotnet build worked during the original preview implementation
  • Re-test the current dotnet new maui template with NativeAOT

List of TODOs:

Activity

  1. added this to the .NET 10 milestone on Feb 12, 2025
  2. jonpryor commented on Feb 12, 2025

    @jonpryor
    Contributor

    Alternate "MVP 1.1" typemap idea, for posterity: instead of a Dictionary<string, Type>, have two fields:

    1. Dictionary<string, RuntimeTypeHandle> or string[]+RuntimeTypeHandle[] pair, plus
    2. Dictionary<string, Type>

    The linker step would populate (1), which might be faster because RuntimeTypeHandle is 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?
  3. jonathanpeppers commented on Feb 12, 2025

    @jonathanpeppers
    MemberAuthor

    There must be a way to use a TypeHandle with Mono.Cecil, as the IL "typemap MVP 1.0" is writing is doing this:

    /*
    * 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);

  4. filipnavara commented on Feb 12, 2025

    @filipnavara
    Member

    Ldtoken returns RuntimeTypeHandle so 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.

  5. simonrozsival commented on Feb 21, 2025

    @simonrozsival
    Member

    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?

  6. jonathanpeppers commented on Feb 21, 2025

    @jonathanpeppers
    MemberAuthor

    @simonrozsival brought up a point that the RuntimeTypeHandle values 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
  7. simonrozsival commented on Feb 21, 2025

    @simonrozsival
    Member

    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> using il.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 RuntimeTypeHandle while we're processing the IL and use it for lookups via Type.GetTypeFromHandle at 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
  8. filipnavara commented on Feb 21, 2025

    @filipnavara
    Member

    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)

  9. added theissue type on Apr 3, 2025
  10. am11 commented on Apr 13, 2025

    @am11
    Member

    For zero-alloc string->Type dictionary known at build-time, we can generate a lookup method using stack-based ReadOnlySpan and collection expressions like https://dotnetfiddle.net/1P6yNL (replace seedDataDictionary with 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 AdvSIMD
    
    Method 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

  11. jonathanpeppers commented on Apr 15, 2025

    @jonathanpeppers
    MemberAuthor

    @am11 we have an implementation that doesn't use Dictionary at all now, so some of the comments above are out of date:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Area: App+Library BuildIssues when building Library projects or Application projects.Area: NativeAOTIssues that only occur when using NativeAOT.

Type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions