Use nearest prime sizing for collection trimming - #132098
Conversation
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
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. |
|
Tagging subscribers to this area: @dotnet/area-system-collections |
There was a problem hiding this comment.
Pull request overview
Updates internal hash-table sizing helpers so Dictionary<TKey, TValue>.TrimExcess(int) and HashSet<T>.TrimExcess(int) choose a tighter “nearest valid prime” capacity (rather than the growth-oriented cached prime table), reducing retained storage after trimming while keeping the existing cached-prime behavior for construction/growth paths.
Changes:
- Added a new prime-sizing path (
GetPrimeAtLeast) and optimized prime search inSystem.Collections.HashHelpers. - Switched
Dictionary/HashSetTrimExcess(int)to use the tighter prime sizing and refactored trim rehashing into compacting resize helpers. - Added regression tests asserting the new
TrimExcesscapacity selection behavior.
Show a summary per file
| File | Description |
|---|---|
| src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/General/HashHelpers.cs | Optimizes prime selection in MetadataLoadContext’s local HashHelpers. |
| src/libraries/System.Private.CoreLib/src/System/Collections/HashHelpers.cs | Adds GetPrimeAtLeast and updates prime-testing/search logic used by core collections. |
| src/libraries/System.Private.CoreLib/src/System/Collections/Generic/HashSet.cs | Uses tighter prime sizing for TrimExcess and introduces compacting resize/copy helpers. |
| src/libraries/System.Private.CoreLib/src/System/Collections/Generic/Dictionary.cs | Uses tighter prime sizing for TrimExcess and introduces compacting resize helper. |
| src/libraries/System.Collections/tests/Generic/HashSet/HashSet.Generic.Tests.cs | Adds test coverage for the new HashSet.TrimExcess sizing behavior. |
| src/libraries/System.Collections/tests/Generic/Dictionary/Dictionary.Generic.Tests.cs | Adds test coverage for the new Dictionary.TrimExcess sizing behavior. |
Review details
- Files reviewed: 6/6 changed files
- Comments generated: 1
- Review effort level: Lite
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Unfortunately, there are still major problems with the current version of this PR:
As the situation stands today, the current version of this PR is clearly and definitely not something that should be approved. |
d8ccc7f to
131ba6c
Compare
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
131ba6c to
609a2f2
Compare
There was a problem hiding this comment.
Review details
Suppressed comments (1)
src/libraries/Common/src/System/Collections/HashHelpers.cs:146
GetPrimeAtLeastCoreuses an unboundedwhile (true)and incrementscandidatewithout any overflow/termination check. For largeminvalues (e.g.,int.MaxValuefrom publicDictionary.TrimExcess(int)/HashSet.TrimExcess(int)),candidate += incrementcan overflow to a negative value and the loop will never terminate, potentially hanging the caller.
- Files reviewed: 18/18 changed files
- Comments generated: 0 new
- Review effort level: Lite
|
Hi all, I was just wondering, why we do not make the prime array denser? If we increase the size 10 times, we should lower the overhead to 1/10 of the current. The performance hit for searching the correct prime should be neglible to the overall cost of copy of all elements (if using linear search). It may be noticable, if someone is allocating large dictionaries and then not using them. |
This comes at other cost, including pessimizing the common case of automatic growth in favor of the less common case of explicit user sizing. There's a lot of primes, with the largest gap being (when accounting for the
Users intentionally over-allocating isn't a concern. That's on them to fix if its problematic. |
I appreciate the feedback and while I've already addressed many of these statements on the other thread, I'll summarize the general response here as well. Given effectively random hashes (which is expected for most inputs and is how most The existing lookup table entries averages about a 1.2 growth between prime entries and so the best case scenario when using exclusively the lookup table is around Then, while the Using This then changes nothing with regards to what users could already see/experience. That is, the perf characteristics here are entirely dependent on the There are few things that are true wins or true losses, most are balanced based on many factors. This PR in particular has one easy win in improving the prime lookup perf, at the cost of more code and algorithmic complexity. It then has a different win in allowing TrimExcess to respect the user specified size (rather than choosing something that is in up to 1.2x larger), but at the cost of meaning users explicitly sizing their dictionaries need to better ensure they're picking an appropriate size -- which again is something they already had to consider for many specified capacities, just not all of them. |
|
This is also notably not something for .NET 11, it'd be for .NET 12; and so it's something we have essentially a full 15 months to get feedback around and to tune if needed, including if we simply want to always choose |
Dictionary<TKey, TValue>andHashSet<T>currently use the growth-oriented cached prime table forTrimExcess, which can retain nearly 20% more storage than requested. Select the nearest valid prime for compaction while preserving the existing cached-prime behavior for construction,EnsureCapacity, and automatic growth.The shared prime-selection implementation now lives under
libraries/Commonrather than being duplicated by MetadataLoadContext.Internal prime search
These are microbenchmarks of the computed-prime fallback beyond the precomputed cache. They measure the helper directly, not end-user
TrimExcessperformance. Visiting only6k +/- 1candidates and reusing the square-root limit improved representative searches by 33-36% locally:TrimExcessTo isolate destination sizing from other implementation differences, these benchmarks use the PR implementation for both cases and compact to either the previous cached-prime target or the new nearest-prime target. Copy throughput is mixed within a few percent while allocation falls with the retained capacity:
Dictionary<int, int>Dictionary<int, int>Dictionary<int, int>HashSet<int>HashSet<int>HashSet<int>At 5,001,234 entries, this avoids 998,220 slots -- approximately 19.04 MiB for
Dictionary<int, int>or 15.23 MiB forHashSet<int>across the bucket and entry arrays.Addresses the
TrimExcesssizing problem discussed in #132051.Note
This pull request description was drafted with GitHub Copilot.