You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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).
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:
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.
Activity
simonrozsival commentedon Mar 26, 2026
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, sameTypeHashingAlgorithms.ComputeNameHashCodehash, same ASCII fast-path key comparison asNativeReader.StringEquals).Two end-to-end paths benchmarked
Path A — current approach:
Path B — proposed approach:
Results (.NET 10, Apple M1 Max, Arm64 RyuJIT)
LookupViaStringjava/lang/String(16B)LookupViaSpanjava/lang/String(16B)LookupViaStringcom/example/pkg/ParentClass$ChildClass(38B)LookupViaSpancom/example/pkg/ParentClass$ChildClass(38B)LookupViaStringcom/example/pkg/some/deep/nested/SomeFactory(44B)LookupViaSpancom/example/pkg/some/deep/nested/SomeFactory(44B)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
Typereference 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-basedTryGetValueNativeParser.StringEquals(NativeFormatReader.String.cs:29) — addStringEquals(ReadOnlySpan<char>)overloadTypeHashingAlgorithms.ComputeNameHashCode(TypeHashingAlgorithms.cs:29) — addReadOnlySpan<char>overload (trivial; the algorithm already works character-by-character)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 overloadNote: 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 theAscii.ToUtf16step entirely. That would requireComputeNameHashCode(ReadOnlySpan<byte>)(already exists asComputeASCIINameHashCodein the tools, with a small bug on line 60) andStringEquals(ReadOnlySpan<byte>)onNativeParser.[TrimmableTypeMap] Reuse base UCO wrappers for inherited overrides (#…