From f11e59561209a6cbea98f6625057de2535814b06 Mon Sep 17 00:00:00 2001 From: Art Leonard Date: Sun, 19 Jul 2026 00:14:43 -0700 Subject: [PATCH 1/3] Speed up case-insensitive Enum parsing Replace the per-token O(N) linear EqualsOrdinalIgnoreCase scan in TryParseByName with a lazily-built, cached OrdinalIgnoreCase Dictionary on the per-type EnumInfo, consumed via AlternateLookup> to avoid string allocation. Names are inserted via TryAdd in value-sorted order, preserving the original first-match (lowest-value-wins) behavior for names differing only by case. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d7060115-2ee5-46f4-b6e0-acad31d5a484 --- .../src/System/Enum.EnumInfo.cs | 30 +++++++++++++++++++ .../System.Private.CoreLib/src/System/Enum.cs | 17 ++++++----- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Enum.EnumInfo.cs b/src/libraries/System.Private.CoreLib/src/System/Enum.EnumInfo.cs index 07a205ce3016bd..adfc20e36f05d6 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Enum.EnumInfo.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Enum.EnumInfo.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; using System.Diagnostics; using System.Numerics; using System.Runtime.InteropServices; @@ -17,6 +18,10 @@ internal sealed partial class EnumInfo public readonly TStorage[] Values; public readonly string[] Names; + // Lazily-built, cached case-insensitive name-to-value lookup used to accelerate + // case-insensitive parsing. Null until the first case-insensitive parse of this enum type. + private Dictionary? _namesToValuesIgnoreCase; + // Each entry contains a list of sorted pair of enum field names and values, sorted by values public EnumInfo(bool hasFlagsAttribute, TStorage[] values, string[] names) { @@ -32,6 +37,31 @@ public EnumInfo(bool hasFlagsAttribute, TStorage[] values, string[] names) ValuesAreSequentialFromZero = AreSequentialFromZero(values); } + /// + /// Gets a case-insensitive name-to-value lookup, building and caching it on first use. + /// + public Dictionary GetNamesToValuesIgnoreCase() + { + return _namesToValuesIgnoreCase ??= CreateNamesToValuesIgnoreCase(); + + Dictionary CreateNamesToValuesIgnoreCase() + { + string[] names = Names; + TStorage[] values = Values; + var lookup = new Dictionary(names.Length, StringComparer.OrdinalIgnoreCase); + + // Insert in Names order (which is sorted by value). For names that differ only by case, + // TryAdd keeps the first (lowest-value) entry, matching the original linear scan's + // "first match wins" behavior. + for (int i = 0; i < names.Length; i++) + { + lookup.TryAdd(names[i], values[i]); + } + + return lookup; + } + } + /// Create a copy of . public unsafe TResult[] CloneValues() where TResult : struct { diff --git a/src/libraries/System.Private.CoreLib/src/System/Enum.cs b/src/libraries/System.Private.CoreLib/src/System/Enum.cs index e21515e05d3faf..6b0bdc323cec47 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Enum.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Enum.cs @@ -6,6 +6,7 @@ #define RARE_ENUMS #endif +using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; @@ -1052,6 +1053,12 @@ private static bool TryParseByName(RuntimeType enumType, ReadOnlySpan< bool parsed = true; TStorage localResult = default; + + // For case-insensitive parsing, use a cached name->value lookup and match spans without allocating. + Dictionary.AlternateLookup> ignoreCaseLookup = ignoreCase ? + enumInfo.GetNamesToValuesIgnoreCase().GetAlternateLookup>() : + default; + while (value.Length > 0) { // Find the next separator. @@ -1080,14 +1087,10 @@ private static bool TryParseByName(RuntimeType enumType, ReadOnlySpan< bool success = false; if (ignoreCase) { - for (int i = 0; i < enumNames.Length; i++) + if (ignoreCaseLookup.TryGetValue(subvalue, out TStorage matchedValue)) { - if (subvalue.EqualsOrdinalIgnoreCase(enumNames[i])) - { - localResult |= enumValues[i]; - success = true; - break; - } + localResult |= matchedValue; + success = true; } } else From 511974e8346aa87e3dff2ca433940d80c899cbff Mon Sep 17 00:00:00 2001 From: Art Leonard Date: Sun, 19 Jul 2026 12:13:36 -0700 Subject: [PATCH 2/3] Fix NativeAOT build and harden case-insensitive Enum lookup cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shared Enum.cs binds unqualified EnumInfo to System.Reflection.EnumInfo under NativeAOT (via using System.Reflection;), so the new GetNamesToValuesIgnoreCase() must exist on that type too — add it to NativeAOT's EnumInfo to fix the CoreCLR_Bootstrapped/NativeAOT compile break. Also harden lazy publication of the cached OrdinalIgnoreCase lookup in both EnumInfo types: replace the non-atomic ??= with Interlocked.CompareExchange so concurrent first-parse callers observe a single published dictionary instance. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d7060115-2ee5-46f4-b6e0-acad31d5a484 --- .../src/System/Reflection/EnumInfo.cs | 34 +++++++++++++++++++ .../src/System/Enum.EnumInfo.cs | 11 ++++-- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/EnumInfo.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/EnumInfo.cs index 71f1c69ffe5018..93048052f3fe47 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/EnumInfo.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/EnumInfo.cs @@ -8,6 +8,7 @@ using System.Runtime; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Threading; namespace System.Reflection { @@ -41,6 +42,39 @@ public EnumInfo(Type underlyingType, TStorage[] values, string[] names, bool isF internal TStorage[] Values { get; } internal bool ValuesAreSequentialFromZero { get; } + // Lazily-built, cached case-insensitive name-to-value lookup used to accelerate + // case-insensitive parsing. Null until the first case-insensitive parse of this enum type. + private Dictionary? _namesToValuesIgnoreCase; + + /// + /// Gets a case-insensitive name-to-value lookup, building and caching it on first use. + /// + public Dictionary GetNamesToValuesIgnoreCase() + { + return _namesToValuesIgnoreCase ?? Initialize(); + + Dictionary Initialize() + { + string[] names = Names; + TStorage[] values = Values; + var lookup = new Dictionary(names.Length, StringComparer.OrdinalIgnoreCase); + + // Insert in Names order (which is sorted by value). For names that differ only by case, + // TryAdd keeps the first (lowest-value) entry, matching the original linear scan's + // "first match wins" behavior. + for (int i = 0; i < names.Length; i++) + { + lookup.TryAdd(names[i], values[i]); + } + + // Publish atomically. If another thread raced and already published a lookup, + // reuse theirs so every caller observes a single cached instance. The plain + // read on the fast path above is safe: the reference is published with release + // semantics and readers reach the contents through a data-dependent access. + return Interlocked.CompareExchange(ref _namesToValuesIgnoreCase, lookup, null) ?? lookup; + } + } + /// Create a copy of . public unsafe TResult[] CloneValues() where TResult : struct { diff --git a/src/libraries/System.Private.CoreLib/src/System/Enum.EnumInfo.cs b/src/libraries/System.Private.CoreLib/src/System/Enum.EnumInfo.cs index adfc20e36f05d6..6e161a8b1dcf23 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Enum.EnumInfo.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Enum.EnumInfo.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Numerics; using System.Runtime.InteropServices; +using System.Threading; namespace System { @@ -42,9 +43,9 @@ public EnumInfo(bool hasFlagsAttribute, TStorage[] values, string[] names) /// public Dictionary GetNamesToValuesIgnoreCase() { - return _namesToValuesIgnoreCase ??= CreateNamesToValuesIgnoreCase(); + return _namesToValuesIgnoreCase ?? Initialize(); - Dictionary CreateNamesToValuesIgnoreCase() + Dictionary Initialize() { string[] names = Names; TStorage[] values = Values; @@ -58,7 +59,11 @@ Dictionary CreateNamesToValuesIgnoreCase() lookup.TryAdd(names[i], values[i]); } - return lookup; + // Publish atomically. If another thread raced and already published a lookup, + // reuse theirs so every caller observes a single cached instance. The plain + // read on the fast path above is safe: the reference is published with release + // semantics and readers reach the contents through a data-dependent access. + return Interlocked.CompareExchange(ref _namesToValuesIgnoreCase, lookup, null) ?? lookup; } } From 8e0a149dd494f3a76ab9029ff8f4bf7be2481b4f Mon Sep 17 00:00:00 2001 From: Art Leonard Date: Sun, 19 Jul 2026 18:54:58 -0700 Subject: [PATCH 3/3] Use Volatile.Read on the case-insensitive Enum lookup fast path Pairs the fast-path acquire read with the existing Interlocked.CompareExchange release publish in both EnumInfo implementations (shared CoreCLR/Mono and NativeAOT), matching the runtime convention for lazily published cross-thread fields and resolving the reviewer's memory-model concern. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d7060115-2ee5-46f4-b6e0-acad31d5a484 --- .../src/System/Reflection/EnumInfo.cs | 8 ++++---- .../System.Private.CoreLib/src/System/Enum.EnumInfo.cs | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/EnumInfo.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/EnumInfo.cs index 93048052f3fe47..2b6a284ee9a4c8 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/EnumInfo.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/EnumInfo.cs @@ -51,7 +51,7 @@ public EnumInfo(Type underlyingType, TStorage[] values, string[] names, bool isF /// public Dictionary GetNamesToValuesIgnoreCase() { - return _namesToValuesIgnoreCase ?? Initialize(); + return Volatile.Read(ref _namesToValuesIgnoreCase) ?? Initialize(); Dictionary Initialize() { @@ -68,9 +68,9 @@ Dictionary Initialize() } // Publish atomically. If another thread raced and already published a lookup, - // reuse theirs so every caller observes a single cached instance. The plain - // read on the fast path above is safe: the reference is published with release - // semantics and readers reach the contents through a data-dependent access. + // reuse theirs so every caller observes a single cached instance. The fast-path + // Volatile.Read pairs an acquire load with this release publish so readers always + // observe a fully-initialized dictionary. return Interlocked.CompareExchange(ref _namesToValuesIgnoreCase, lookup, null) ?? lookup; } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Enum.EnumInfo.cs b/src/libraries/System.Private.CoreLib/src/System/Enum.EnumInfo.cs index 6e161a8b1dcf23..6633a2db6549b5 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Enum.EnumInfo.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Enum.EnumInfo.cs @@ -43,7 +43,7 @@ public EnumInfo(bool hasFlagsAttribute, TStorage[] values, string[] names) /// public Dictionary GetNamesToValuesIgnoreCase() { - return _namesToValuesIgnoreCase ?? Initialize(); + return Volatile.Read(ref _namesToValuesIgnoreCase) ?? Initialize(); Dictionary Initialize() { @@ -60,9 +60,9 @@ Dictionary Initialize() } // Publish atomically. If another thread raced and already published a lookup, - // reuse theirs so every caller observes a single cached instance. The plain - // read on the fast path above is safe: the reference is published with release - // semantics and readers reach the contents through a data-dependent access. + // reuse theirs so every caller observes a single cached instance. The fast-path + // Volatile.Read pairs an acquire load with this release publish so readers always + // observe a fully-initialized dictionary. return Interlocked.CompareExchange(ref _namesToValuesIgnoreCase, lookup, null) ?? lookup; } }