Pre-size intermediate Dictionary in ToFrozenDictionary#128300
Open
AndrewP-GH wants to merge 5 commits into
Open
Pre-size intermediate Dictionary in ToFrozenDictionary#128300AndrewP-GH wants to merge 5 commits into
AndrewP-GH wants to merge 5 commits into
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Improves ToFrozenDictionary construction performance by pre-sizing the intermediate Dictionary when the source reports a count, and adds a regression test to ensure duplicate-key “last in wins” semantics remain intact for ICollection sources.
Changes:
- Pre-size the intermediate
Dictionary<TKey, TValue>using the source collection’s count when available. - Add a unit test covering duplicate keys for
ICollectionsources to prevent regressions in overwrite semantics.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenDictionary.cs | Pre-sizes intermediate dictionary based on source count to reduce resizes/rehashes during population. |
| src/libraries/System.Collections.Immutable/tests/Frozen/FrozenDictionaryTests.cs | Adds regression coverage to ensure pre-sizing doesn’t change duplicate-key overwrite behavior. |
Contributor
|
Tagging subscribers to this area: @dotnet/area-system-collections |
1f15afe to
1ce0801
Compare
When the source is not already a Dictionary, ToFrozenDictionary builds an intermediate Dictionary to deduplicate keys with last-wins semantics. The previous code used the default-capacity constructor, which paid log2(N) resizes and ~2N redundant rehashes when populating from a known size source such as an array or List. Use ICollection<KeyValuePair>.Count as a capacity hint when available, mirroring the HashSet(IEnumerable, IEqualityComparer) constructor. The ReadOnlySpan overload of FrozenDictionary.Create already pre-sizes, so this brings the IEnumerable path in line with it.
1ce0801 to
cbeb324
Compare
Switch the source-type check from ICollection<KeyValuePair> to IDictionary<,> / IReadOnlyDictionary<,>. For dictionary inputs Count reflects already-unique keys, so the intermediate Dictionary is sized exactly without risk of over-allocation from duplicate-bearing collections such as KeyValuePair[].
Author
|
@dotnet-policy-service agree company="Dodo Brands" |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
IDictionary<TKey, TValue> extends ICollection<KeyValuePair<TKey, TValue>> and IReadOnlyDictionary<TKey, TValue> extends IReadOnlyCollection<KeyValuePair<TKey, TValue>>, and Count is inherited from the collection interfaces. The dictionary arms always matched the collection arms with the same value, so they were dead code. Matches the pattern Dictionary<,> and ConcurrentDictionary<,> use in their own IEnumerable constructors.
This was referenced May 17, 2026
This comment was marked as duplicate.
This comment was marked as duplicate.
Member
using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Linq;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
BenchmarkSwitcher.FromAssembly(typeof(Bench).Assembly).Run(args);
[MemoryDiagnoser]
public class Bench
{
[Params(16, 256, 4096)]
public int Count { get; set; }
private KeyValuePair<int, int>[] _array = default!;
private List<KeyValuePair<int, int>> _list = default!;
private ConcurrentDictionary<int, int> _concurrent = default!;
private SortedDictionary<int, int> _sorted = default!;
private ImmutableDictionary<int, int> _immutable = default!;
private ReadOnlyDictionary<int, int> _readOnly = default!;
// Duplicate-heavy source: only ~10 distinct keys, repeated Count times.
private KeyValuePair<int, int>[] _arrayDuplicates = default!;
[GlobalSetup]
public void Setup()
{
_array = Enumerable.Range(0, Count).Select(i => new KeyValuePair<int, int>(i, i)).ToArray();
_list = _array.ToList();
_concurrent = new ConcurrentDictionary<int, int>(_array);
_sorted = new SortedDictionary<int, int>(_array.ToDictionary(k => k.Key, v => v.Value));
_immutable = ImmutableDictionary.CreateRange(_array);
_readOnly = new ReadOnlyDictionary<int, int>(_array.ToDictionary(k => k.Key, v => v.Value));
_arrayDuplicates = Enumerable.Range(0, Count)
.Select(i => new KeyValuePair<int, int>(i % 10, i))
.ToArray();
}
[Benchmark] public FrozenDictionary<int, int> FromArray() => _array.ToFrozenDictionary();
[Benchmark] public FrozenDictionary<int, int> FromList() => _list.ToFrozenDictionary();
[Benchmark] public FrozenDictionary<int, int> FromConcurrentDict() => _concurrent.ToFrozenDictionary();
[Benchmark] public FrozenDictionary<int, int> FromSortedDict() => _sorted.ToFrozenDictionary();
[Benchmark] public FrozenDictionary<int, int> FromImmutableDict() => _immutable.ToFrozenDictionary();
[Benchmark] public FrozenDictionary<int, int> FromReadOnlyDict() => _readOnly.ToFrozenDictionary();
[Benchmark] public FrozenDictionary<int, int> FromArrayWithDuplicates() => _arrayDuplicates.ToFrozenDictionary();
} |
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.
ToFrozenDictionarybuilds an intermediateDictionary<,>to deduplicate keys when the source isn't already a matchingDictionary<,>. It used the default-capacity constructor, so a known-size source payslog2(N)resizes and ~2Nrehashes for nothing.Helps most where the early
source as Dictionary<,>reuse path doesn't fire:ConcurrentDictionary<,>,SortedDictionary<,>,ImmutableDictionary<,>,ReadOnlyDictionary<,>, plus arrays andList<KVP>from LINQ.Sizing-only change —
Dictionary<,>capacity is contractually behavior-invariant, so existingMultipleValuesSameKey_LastInSourceWinsalready covers the sharedforeach { d[k] = v; }body.