Skip to content

avoid Keys/Values ToList materialization in DictionaryWrapper#366

Merged
SimonCropp merged 1 commit into
mainfrom
avoid-Keys/Values-ToList-materialization-in-DictionaryWrapper
May 18, 2026
Merged

avoid Keys/Values ToList materialization in DictionaryWrapper#366
SimonCropp merged 1 commit into
mainfrom
avoid-Keys/Values-ToList-materialization-in-DictionaryWrapper

Conversation

@SimonCropp
Copy link
Copy Markdown
Member

The Keys/Values properties (both the generic ICollection and the
non-generic IDictionary surface) materialized a full List on every
access via Cast().ToList() / ToList(). Replace with a lazy
ReadOnlyCollectionWrapper that holds the source IEnumerable plus
a snapshot Count and defers enumeration to the caller.

Benchmark (net11.0 in-process, DictionaryWrapperKeysBenchmark):

Access only (no iteration):
  Hashtable.Keys      Count=500: 16,541 ns / 4160 B ->  43 ns /  80 B
  ReadOnly.Keys       Count=500:  4,060 ns / 4056 B ->  33 ns /  32 B
  IDictionary.Keys    Count=500:  3,735 ns / 4056 B ->  19 ns /  32 B

Access + foreach:
  Hashtable.Keys      Count=500: 17,530 ns / 4160 B -> 16,443 ns / 136 B
  Hashtable.Keys      Count=5  :    228 ns /  200 B ->    198 ns / 136 B

Allocations drop from O(N) to a flat 32-80 B regardless of dictionary
size. CPU is dominated by the underlying Cast iterator on the iterate
path, so wall-time wins there are modest; the pure-access path (e.g.
reflection / introspection callers that never enumerate) is 100-400x
faster at Count=500.

  The Keys/Values properties (both the generic ICollection<T> and the
  non-generic IDictionary surface) materialized a full List<T> on every
  access via Cast<T>().ToList() / ToList(). Replace with a lazy
  ReadOnlyCollectionWrapper<T> that holds the source IEnumerable<T> plus
  a snapshot Count and defers enumeration to the caller.

  Benchmark (net11.0 in-process, DictionaryWrapperKeysBenchmark):

    Access only (no iteration):
      Hashtable.Keys      Count=500: 16,541 ns / 4160 B ->  43 ns /  80 B
      ReadOnly.Keys       Count=500:  4,060 ns / 4056 B ->  33 ns /  32 B
      IDictionary.Keys    Count=500:  3,735 ns / 4056 B ->  19 ns /  32 B

    Access + foreach:
      Hashtable.Keys      Count=500: 17,530 ns / 4160 B -> 16,443 ns / 136 B
      Hashtable.Keys      Count=5  :    228 ns /  200 B ->    198 ns / 136 B

  Allocations drop from O(N) to a flat 32-80 B regardless of dictionary
  size. CPU is dominated by the underlying Cast iterator on the iterate
  path, so wall-time wins there are modest; the pure-access path (e.g.
  reflection / introspection callers that never enumerate) is 100-400x
  faster at Count=500.
@SimonCropp SimonCropp added this to the 0.34.0 milestone May 18, 2026
@SimonCropp SimonCropp merged commit cc8fc76 into main May 18, 2026
4 of 6 checks passed
@SimonCropp SimonCropp deleted the avoid-Keys/Values-ToList-materialization-in-DictionaryWrapper branch May 18, 2026 12:23
@github-actions github-actions Bot restored the avoid-Keys/Values-ToList-materialization-in-DictionaryWrapper branch May 18, 2026 12:23
@SimonCropp SimonCropp deleted the avoid-Keys/Values-ToList-materialization-in-DictionaryWrapper branch May 18, 2026 12:23
This was referenced May 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant