Cache the culture sensitivity of the types - #1396
Merged
Merged
Conversation
CultureSensitiveFormattingContext.GetCultureSensitivity(ITypeSymbol,
CultureSensitiveOptions) is a pure function of the type symbol and the
options, but it was recomputed for every node. It runs about twenty symbol
comparisons and several hierarchy walks, and reaches
HasToStringWithFormatProvider, which enumerated every member of every type
of the base chain through LINQ iterators. Four analyzers that are enabled
by default drive it, so this ran on every invocation, interpolated string
and binary operation of a compilation.
Memoize the result in a ConcurrentDictionary keyed by the type symbol and
the options, the same way AwaitableTypes caches IsAwaitable. The key uses
the full options flags rather than the single flag the method reads today:
the flags are bounded to sixteen combinations, so it costs nothing and
remains correct if another flag starts to matter.
Also replace the LINQ chain of HasToStringWithFormatProvider with a loop
over GetMembers("ToString") up the base chain. This is equivalent, as the
GetAllMembers() overload without a name walks the base chain only.
This was referenced Sep 6, 2026
Closed
Closed
This was referenced Sep 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
CultureSensitiveFormattingContext.GetCultureSensitivity(ITypeSymbol, CultureSensitiveOptions)is a pure function of the type symbol and the options, but nothing was cached: it was fully recomputed for every node.Each call runs about twenty symbol comparisons plus several
IsOrInheritsFrom/IsOrImplementshierarchy walks, and reachesHasToStringWithFormatProvider, which wastype.GetAllMembers().OfType<IMethodSymbol>().Any(...)— ayield returniterator enumerating every member of every type in the base chain, wrapped in two more LINQ iterators, with a per-memberIsOrInheritsFromon the parameter type.Four analyzers enabled by default drive this (
UseIFormatProviderAnalyzer,DoNotUseImplicitCultureSensitiveToStringAnalyzer,DoNotUseToStringIfObjectAnalyzer,SimplifyStringCreateWhenAllParametersAreCultureInvariantAnalyzer), so it ran on every invocation, interpolated string and binary operation of a compilation, and it is worst on codebases with deep type hierarchies where the member walk is longest.Two changes:
ConcurrentDictionarykeyed by the type symbol and the options, the same wayAwaitableTypescachesIsAwaitable. The body was split into a cached front door plusGetCultureSensitivityCore.HasToStringWithFormatProviderwith a plain loop overGetMembers(nameof(ToString))up the base chain.Notes for the reviewer
(typeSymbol, options)iscompilation, which is fixed for the lifetime of a context instance, so the answer is stable for a compilation.IsCultureSensitiveTypeUsingAttributereadscompilation.Assembly.GetAttributes()and is pure for the same reason.UnwrapNullableOfTflag the method reads today. The flags are bounded to sixteen combinations, so the extra entries cost nothing, and the cache stays correct if another flag starts to matter.UnwrapNullableOfTgenuinely has to be part of the key: with it,int?returns the sensitivity ofint, without it the one ofNullable<int>.TryGetValue+ indexer rather thanGetOrAdd(key, factory), to avoid a delegate allocation on every lookup — theGetOrAdd<TArg>overload that would avoid a closure is not available onnetstandard2.0. The computation is idempotent, so a race just recomputes.HasToStringWithFormatProviderrewrite is equivalent, not a behavior change. TheGetAllMembers()overload without a name walks the base chain only, so the loop matches it. TheGetAllMembers(name)overload would not have been equivalent: it additionally walksAllInterfacesfor interface symbols, which would have made an interface inheriting aToString(IFormatProvider)reportCultureSensitiveinstead ofMaybeCultureSensitiveOpaqueRuntimeType, sinceIsFormattableTypeis checked beforeIsOpaqueRuntimeType.GetBestTypeByMetadataNamecalls per compilation — negligible next to the per-node work this removes.Testing
dotnet test --max-parallel-test-modules 2over the five test projects: 19258 passed, 0 failed.dotnet run --project src/DocumentationGeneratorexits 0 with no markdown change: the change is internal and does not affect any rule behavior or documentation.