Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Runtime;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
using System.Threading;


namespace System.Reflection
{
Expand Down Expand Up @@ -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<string, TStorage>? _namesToValuesIgnoreCase;

@MihaZupan MihaZupan Jul 20, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can store the result of GetAlternateLookup instead to avoid the cost of calling it for every lookup.

The difference in the fixed per-lookup overhead may also change where the crossover between a linear scan vs. dictionary would be.


/// <summary>
/// Gets a case-insensitive name-to-value lookup, building and caching it on first use.
/// </summary>
public Dictionary<string, TStorage> GetNamesToValuesIgnoreCase()
{
return Volatile.Read(ref _namesToValuesIgnoreCase) ?? Initialize();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return Volatile.Read(ref _namesToValuesIgnoreCase) ?? Initialize();
return _namesToValuesIgnoreCase ?? Initialize();


Dictionary<string, TStorage> Initialize()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These extra dictionary instantiations cost up to 70kB binary size of Native AOT compiled apps: MichalStrehovsky/rt-sz#242

It should be acceptable binary size regression.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is it so much since we get like 8 instantiations max for the common TStorages?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can drill into it with sizescope if you really want to know.

My guess is that there are unrelated places in the app that use interfaces implemented by Dictionary, like IDictionary. We are not able to track the data flow and tell that IDictionary won't be ever used on these Dictionary instantiations. It makes us to generate the code for IDictionary methods on these instantiationw even though they may be ever called. This can be worked around by using unique collection types without unnecessary interfaces and other baggage for these sorts of implementation details. It stops the surprising interactions, but that comes with other issues.

{
string[] names = Names;
TStorage[] values = Values;
var lookup = new Dictionary<string, TStorage>(names.Length, StringComparer.OrdinalIgnoreCase);
Comment on lines +52 to +60

// 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 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;
Comment on lines +70 to +74

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Publish atomically. If another thread raced and already published a lookup,
// 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;
_namesToValuesIgnoreCase = lookup;
return lookup;

There should be no need to synchronize here, we're the only caller and we don't care which exact instance we got

}
}

/// <summary>Create a copy of <see cref="Values"/>.</summary>
public unsafe TResult[] CloneValues<TResult>() where TResult : struct
{
Expand Down
35 changes: 35 additions & 0 deletions src/libraries/System.Private.CoreLib/src/System/Enum.EnumInfo.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
// 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;
using System.Threading;

namespace System
{
Expand All @@ -17,6 +19,10 @@ internal sealed partial class EnumInfo<TStorage>
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<string, TStorage>? _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)
{
Expand All @@ -32,6 +38,35 @@ public EnumInfo(bool hasFlagsAttribute, TStorage[] values, string[] names)
ValuesAreSequentialFromZero = AreSequentialFromZero(values);
}

/// <summary>
/// Gets a case-insensitive name-to-value lookup, building and caching it on first use.
/// </summary>
public Dictionary<string, TStorage> GetNamesToValuesIgnoreCase()
{
return Volatile.Read(ref _namesToValuesIgnoreCase) ?? Initialize();

Dictionary<string, TStorage> Initialize()
{
string[] names = Names;
TStorage[] values = Values;
var lookup = new Dictionary<string, TStorage>(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++)
Comment on lines +54 to +57
{
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 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;
}
}

/// <summary>Create a copy of <see cref="Values"/>.</summary>
public unsafe TResult[] CloneValues<TResult>() where TResult : struct
{
Expand Down
17 changes: 10 additions & 7 deletions src/libraries/System.Private.CoreLib/src/System/Enum.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#define RARE_ENUMS
#endif

using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
Expand Down Expand Up @@ -1052,6 +1053,12 @@ private static bool TryParseByName<TStorage>(RuntimeType enumType, ReadOnlySpan<

bool parsed = true;
TStorage localResult = default;

// For case-insensitive parsing, use a cached name->value lookup and match spans without allocating.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is counter-intuitive that case-insensitive is faster with this change, but case-sensitive is not. The general expectation is that case-sensitive operations are faster.

Should this be done for case-sensitive parsing as well?

Dictionary<string, TStorage>.AlternateLookup<ReadOnlySpan<char>> ignoreCaseLookup = ignoreCase ?
enumInfo.GetNamesToValuesIgnoreCase().GetAlternateLookup<ReadOnlySpan<char>>() :
default;

while (value.Length > 0)
Comment on lines +1057 to 1062
{
// Find the next separator.
Expand Down Expand Up @@ -1080,14 +1087,10 @@ private static bool TryParseByName<TStorage>(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
Expand Down
Loading