Skip to content

[TrimmableTypeMap] Performance considerations #10795

Description

@simonrozsival

Part of #10788

  • Compare startup of CoreCLR with trimmable typemap
  • Validate Crossgen2 pre-compiled lookups do not add overhead to startup
  • Identify bottlenecks and adjust the design to improve perf
  • Implement UTF-8 alternate lookups or any other possible perf improvements if needed

Activity

  1. added this to the .NET 11 milestone on Feb 10, 2026
  2. simonrozsival commented on Mar 26, 2026

    @simonrozsival
    MemberAuthor

    UTF-8 → span lookup: benchmark data

    Investigated the cost of JNI type-name lookup with and without string allocation, using a managed simulation of the NativeHashtable (keys stored as UTF-8 bytes, same TypeHashingAlgorithms.ComputeNameHashCode hash, same ASCII fast-path key comparison as NativeReader.StringEquals).

    Two end-to-end paths benchmarked

    Path A — current approach:

    UTF-8 ROS<byte>  →  Encoding.UTF8.GetString()  →  string  →  TryGetValue(string)
    

    Path B — proposed approach:

    UTF-8 ROS<byte>  →  Ascii.ToUtf16() into stackalloc char[]  →  TryGetValue(ReadOnlySpan<char>)
    

    Results (.NET 10, Apple M1 Max, Arm64 RyuJIT)

    Method Input Mean Allocated
    LookupViaString java/lang/String (16B) 31.8 ns 56 B
    LookupViaSpan java/lang/String (16B) 20.7 ns 0 B
    LookupViaString com/example/pkg/ParentClass$ChildClass (38B) 67.6 ns 104 B
    LookupViaSpan com/example/pkg/ParentClass$ChildClass (38B) 47.7 ns 0 B
    LookupViaString com/example/pkg/some/deep/nested/SomeFactory (44B) 70.8 ns 112 B
    LookupViaSpan com/example/pkg/some/deep/nested/SomeFactory (44B) 53.9 ns 0 B

    The span path is consistently ~30% faster with zero heap allocation across all input sizes.

    The saving is larger than just the allocation cost (~4 ns) because the string path also pays GC write barrier costs as the returned Type reference flows through the call.

    What would need to change in the runtime

    The optimization requires adding a TryGetValue(ReadOnlySpan<char>) overload to the NativeAOT type-map lookup path. Concretely:

    • ExternalTypeMapDictionary (TypeMapLazyDictionary.NativeAot.cs:136) — add span-based TryGetValue
    • NativeParser.StringEquals (NativeFormatReader.String.cs:29) — add StringEquals(ReadOnlySpan<char>) overload
    • TypeHashingAlgorithms.ComputeNameHashCode (TypeHashingAlgorithms.cs:29) — add ReadOnlySpan<char> overload (trivial; the algorithm already works character-by-character)
    • The public IReadOnlyDictionary<string, Type> interface would need a companion API or the call site would need to downcast to the concrete type to access the span overload

    Note: an even more aggressive optimization would be to add TryGetValue(ReadOnlySpan<byte>) — since JNI names are ASCII, the UTF-8 bytes equal the char values, so the same hash and comparison work on raw bytes, skipping the Ascii.ToUtf16 step entirely. That would require ComputeNameHashCode(ReadOnlySpan<byte>) (already exists as ComputeASCIINameHashCode in the tools, with a small bug on line 60) and StringEquals(ReadOnlySpan<byte>) on NativeParser.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Area: CoreCLRIssues that only occur when using CoreCLR.Area: NativeAOTIssues that only occur when using NativeAOT.needs-triageIssues that need to be assigned.trimmable-type-map

    Type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions