Skip to content

Reduce GetAllServices key discovery allocations (#93) - #105

Merged
AGiorgetti merged 1 commit into
developfrom
codex/93-allservices-discovery-perf
Oct 7, 2026
Merged

AGiorgetti merged 1 commit into
developfrom
codex/93-allservices-discovery-perf

Conversation

@AGiorgetti

Copy link
Copy Markdown
Contributor

GetAllServices key discovery creates a new HashSet on every call, even when the immutable provider snapshot has no keys or only one applicable key group.

Return an empty singleton for no keys and reuse the snapshot's copied read-only collection for exact-only or generic-definition-only keys. Keep a fresh value-deduplicated union when both groups are present. The generic path is already typed from #85; its eager native aggregation and existing list sizing remain intact.

Warmed local allocations on .NET 8, 9 and 10 drop from 1,160 to 360 B/call for generic ten-key discovery (69%), and from 1,864 to 1,064 B/call for the Type overload (43%). Empty and single-group cases also improve. The merged closed/open-generic case remains at 456/712 B (generic/Type); no merged-type or service-instance cache is added. Full results and reproduction are in repro/Issue93.AllServicesAllocation/README.md.

Validation: 14 new contract cases pass against both baseline and candidate on net472/net8.0/net9.0/net10.0. All 1,338 tests pass per target; Release library builds include netstandard2.0. New tests cover metadata/returned-key mutation, equal-key deduplication, eager explicit iterator consumption and failure timing, transient/scoped/singleton behavior. Existing decorator, AnyKey, registration order, value-type and enumerable precedence coverage passes. Existing net472 test dependency support warnings remain. Staged whitespace check passed.

Closes #93.

Co-authored-by: Codex codex@openai.com

Co-authored-by: Codex <codex@openai.com>

Copy link
Copy Markdown
Contributor Author

Implemented solution for #93:

ServiceProviderRegistrationSnapshot.GetKeys now checks exact and generic-definition key groups first:

  • Neither contains keys: return Array.Empty().
  • Only one contains keys: return its existing copied, read-only snapshot collection.
  • Both contain keys: return a fresh HashSet union, preserving value equality and deduplication.
  • The snapshot constructor already copies key storage independently of public mutable ServiceKeys metadata. Returning a read-only collection therefore preserves isolation without allocating another set. Mutating a disposable two-group union cannot change subsequent results. No resolved instances or merged closed-type results are cached.

    The generic GetAllServices path was already typed by #85. The existing eager native aggregation remains unchanged, preserving explicit IEnumerable registrations, ordinary-first/native group order, closed/open registration behavior, AnyKey exclusion, hidden decorator slots and native lifetimes.

    14 new contract cases pass against baseline and candidate on all four targets. The full suite passes 1,338 tests per target (net472, net8.0, net9.0, net10.0), including eager iterator side effects/failure timing, public and returned-key mutation, equal-but-distinct keys, repeated transient resolution and scope isolation. Production changes are confined to ServiceProviderRegistrationSnapshot.cs.

Copy link
Copy Markdown
Contributor Author

Measured allocation gains for #93 (median bytes/call; identical on .NET 8.0.31, 9.0.20 and 10.0.12):

Case Generic before → after Type before → after
Empty 96 → 32 160 → 96
Unkeyed-only 152 → 88 216 → 152
One key 336 → 120 464 → 248
Ten keys 1,160 → 360 1,864 → 1,064
Open-generic keys only 336 → 120 464 → 248
Merged closed/open keys 456 → 456 712 → 712

The ten-key case reduces allocations by approximately 69% for the generic API and 43% for the Type overload. Two-group merges retain their original allocation cost; this PR adds no merged-type cache.

Baseline: c8af7f8. Candidate: d474e64. Release, Microsoft DI 10.0.0, DOTNET_TieredCompilation=0, 1,000 warm-ups and seven samples of 100,000 calls per case. GC.GetAllocatedBytesForCurrentThread measures allocations; provider setup, singleton warm-up, keys, delegates and sample storage are outside sampling. Every measured count is checked against native DI groups.

Known-key eager typed-list controls and group-count controls are included. They omit dynamic discovery; the group-count control returns no aggregate collection. Measurements are local warmed allocation probes, not throughput guarantees or BenchmarkDotNet confidence estimates. No net472 allocation or cold-provider result is claimed.

Reproduction, controls and full results: repro/Issue93.AllServicesAllocation/README.md.

@AGiorgetti
AGiorgetti merged commit ead376e into develop Oct 7, 2026
1 check passed
@AGiorgetti
AGiorgetti deleted the codex/93-allservices-discovery-perf branch October 7, 2026 17:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Reduce warmed GetAllServices discovery allocations with typed aggregation and key fast paths

1 participant