Skip to content

Defer unused CoreLib cache initialization - #132576

Merged
tannergooding merged 4 commits into
dotnet:mainfrom
tannergooding:tannergooding-split-compareinfo-icu-cache
Aug 20, 2026
Merged

Defer unused CoreLib cache initialization#132576
tannergooding merged 4 commits into
dotnet:mainfrom
tannergooding:tannergooding-split-compareinfo-icu-cache

Conversation

@tannergooding

Copy link
Copy Markdown
Member

Moves the ICU-only CompareInfo search values and the EncodingTable name cache behind nested static holders. This preserves one-time initialization when those paths are used while allowing NativeAOT to trim them from applications that only need unrelated static state.

A locally built Release NativeAOT win-x64 Hello World with InvariantGlobalization=true measured:

Build Executable Delta
main 993,280 bytes
CompareInfo holder 971,264 bytes -22,016 bytes
Both holders 968,704 bytes -24,576 bytes

The cumulative raw section changes are .text -12,800 bytes, .rdata -9,728 bytes, .data -1,024 bytes, .pdata -512 bytes, and .reloc -512 bytes. The final dependency graph no longer contains SearchValues.Create, EncodingTable::.cctor, or the ConcurrentDictionary<string, int> name-cache construction.

Note

This pull request was prepared with GitHub Copilot.

tannergooding and others added 2 commits August 20, 2026 10:59
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings August 20, 2026 18:11
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-globalization
See info in area-owners.md if you want to be subscribed.

Copilot AI left a comment

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.

Pull request overview

This PR defers initialization of two rarely-needed CoreLib caches by moving them behind nested static holder types, allowing NativeAOT trimming to omit the cache initialization (and related dependencies) when those paths aren’t used.

Changes:

  • Move the EncodingTable name→codepage ConcurrentDictionary into a nested static holder to avoid initializing it unless GetCodePageFromName is called.
  • Move the ICU-only CompareInfo SearchValues<char> initialization into a nested static holder to avoid creating it unless the ASCII fast-path is exercised.
Show a summary per file
File Description
src/libraries/System.Private.CoreLib/src/System/Text/EncodingTable.cs Defers ConcurrentDictionary<string,int> construction until GetCodePageFromName is invoked.
src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Icu.cs Defers ICU ASCII SearchValues<char> creation until the ordinal helper methods need it.

Review details

  • Files reviewed: 2/2 changed files
  • Comments generated: 0
  • Review effort level: Lite

@tannergooding

Copy link
Copy Markdown
Member Author

A sibling to #132550. Just did some local analysis for obvious static constructor roots that looked unnecessary

-- CC. @MichalStrehovsky, @jkotas, @agocke this is a trivial improvement that's likely worth taking for .NET 11/12, but it also feels like a more general issue and is a really common pattern in how we and the broader ecosystem write code. This is likely worth explicitly handling in the linker, either via some explicit trimming support or a way (maybe even an attribute) to indicate we support it being "outlined" by the tooling into such helper shapes.

We shouldn't have to explicitly think that every unique static readonly field may require its own nested holder class. At worst this should be some analyzer that flags it for NAOT enabled libraries with a fixer if we believe handling it in the linker is not feasible.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 20, 2026 18:49
Comment thread src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Icu.cs Outdated

Copilot AI left a comment

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.

Review details

  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Lite

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 20, 2026 19:09

Copilot AI left a comment

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.

Review details

  • Files reviewed: 2/2 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@tannergooding

Copy link
Copy Markdown
Member Author

Updated everything and savings measured locally are still the same.

@MichalStrehovsky

Copy link
Copy Markdown
Member

-- CC. @MichalStrehovsky, @jkotas, @agocke this is a trivial improvement that's likely worth taking for .NET 11/12, but it also feels like a more general issue and is a really common pattern in how we and the broader ecosystem write code. This is likely worth explicitly handling in the linker, either via some explicit trimming support or a way (maybe even an attribute) to indicate we support it being "outlined" by the tooling into such helper shapes.

I don't know what we could do here. Roslyn dumps these inline into the static constructor so we'd need to determine the field is never read (easy enough) and then: determine this is indeed side effect free (ConcurrentDictionary constructor is a ton of code, SearchValues.Create even uses hardware intrinsics to make things more interesting) and then excise the call and ststfld from the static constructor. I don't think we have capabilities anywhere near to allow this.

We could replace stsfld with pop and hope RyuJIT determines this is side effect free, but it likely won't help with anything that is interesting (like here).

@MichalStrehovsky

Copy link
Copy Markdown
Member

Nice!

Size statistics

Project Size before Size after Difference
TodosApi-linux 25177328 25177328 0
TodosApi-windows 26222592 26222080 -512
avalonia.app-linux 18895952 18887632 -8320
avalonia.app-windows 19238912 19230720 -8192
hello-linux 1279168 1254432 -24736
hello-minimal-linux 1131576 1110936 -20640
hello-minimal-windows 830464 809472 -20992
hello-windows 994816 970752 -24064
kestrel-minimal-linux 5405536 5401440 -4096
kestrel-minimal-windows 4930048 4926976 -3072
reflection-linux 1874056 1849288 -24768
reflection-windows 1738752 1713152 -25600
webapiaot-linux 9887496 9883400 -4096
webapiaot-windows 10367488 10366976 -512
winrt-component-minimal-windows 778752 759296 -19456

@tannergooding

Copy link
Copy Markdown
Member Author

I don't know what we could do here

If nothing else, we should have an (likely opt-in) analyzer for this type of thing and flag it as something that can root code unnecessarily.

But, it could also be something we need to think around and discuss across the language, tooling, and libraries. For example, maybe this just ends up as some [TrimSafe] static readonly T[] s_field = ... and the linker trusts the user isn't lying. Maybe its some advanced analysis and best case handling that the linker does for common scenarios and users have to manually handle the outliers. Maybe its additional warnings about static constructors and it rooting things that may be unused, etc.

There's a lot that could be done here to help avoid these kinds of issues being introduced, even accidentally in the future; and ideally something minimal that can be done so that idiomatic code patterns aren't pessimizing trimability of the ecosystem. -- I'm not saying its going to be easy, just that I think this is a representative example of how impactful a handful of lines can be and so its worth spending some time looking into it deeper, as I imagine there are multiple similar cases in real world apps where we can save multiple KB more.

@jkotas

jkotas commented Aug 20, 2026

Copy link
Copy Markdown
Member

This is not just about trimming. The trimming issues have large overlap with startup time issues.

If you have a static constructor that does unnecessary work eagerly, it is both trimming and startup time problem.

@tannergooding

Copy link
Copy Markdown
Member Author

For sure.

The general idea is that people are going to keep writing the "idiomatic" code, we have lots of it ourselves. So either an analyzer telling them "hey this is actually bad" or some tooling, language feature, etc that "fixes" things for them to be "friendly" should exist so that we can push users (and ourselves) into the pit of success here.

We have source generators, we have IL rewriters, we have analyzers, interceptors, etc. Lots of tools available and I'm sure amongst ourselves we can come up with something to help. One could even imagine that [TrimSafe] static readonly T[] s_field = ... is handled by illink for both trimming or for refactoring into the private class NestedCache { private static s_field = ... } to ensure that each is uniquely initialized; or some language feature that does this, or some runtime feature that does a specialized mapping so that no additional IL type is needed;

Options exist and we have enough experts to find a good path forward, whatever that might be.

@tannergooding
tannergooding merged commit ccf5c90 into dotnet:main Aug 20, 2026
127 checks passed
@tannergooding
tannergooding deleted the tannergooding-split-compareinfo-icu-cache branch August 20, 2026 23:55
tannergooding added a commit that referenced this pull request Aug 21, 2026
Backports the complete logical change from #132576.

Original commits:
- 8603450
- aa6346d
- a55f9e2
- dc72d63

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants