Thanks to visit codestin.com
Credit goes to github.com

Skip to content

NativeAOT TODO list #9784

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

Open
6 of 14 tasks
jonathanpeppers opened this issue Feb 12, 2025 · 9 comments
Open
6 of 14 tasks

NativeAOT TODO list #9784

jonathanpeppers opened this issue Feb 12, 2025 · 9 comments
Assignees
Labels
Area: App+Library Build Issues when building Library projects or Application projects.
Milestone

Comments

@jonathanpeppers
Copy link
Member

jonathanpeppers commented Feb 12, 2025

What do we need to do for NativeAOT?

To investigate next:

  • dotnet publish doesn't work, dotnet build does work
  • dotnet new maui template stopped working in main at some point

List of TODOs:

@jonathanpeppers jonathanpeppers added the Area: App+Library Build Issues when building Library projects or Application projects. label Feb 12, 2025
@jonathanpeppers jonathanpeppers self-assigned this Feb 12, 2025
@dotnet-policy-service dotnet-policy-service bot added the needs-triage Issues that need to be assigned. label Feb 12, 2025
@jonathanpeppers jonathanpeppers removed the needs-triage Issues that need to be assigned. label Feb 12, 2025
@jonathanpeppers jonathanpeppers added this to the .NET 10 milestone Feb 12, 2025
@jonpryor
Copy link
Member

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?

@jonathanpeppers
Copy link
Member Author

jonathanpeppers commented Feb 12, 2025

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

@filipnavara
Copy link
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.

@simonrozsival
Copy link
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?

@jonathanpeppers
Copy link
Member Author

@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

@simonrozsival
Copy link
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

@filipnavara
Copy link
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)

@am11
Copy link
Member

am11 commented Apr 13, 2025

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

@jonathanpeppers
Copy link
Member Author

@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
Labels
Area: App+Library Build Issues when building Library projects or Application projects.
Projects
None yet
Development

No branches or pull requests

5 participants