Repository navigation
Reduce GetAllServices key discovery allocations (#93) - #105
Conversation
Co-authored-by: Codex <codex@openai.com>
|
Implemented solution for #93: ServiceProviderRegistrationSnapshot.GetKeys now checks exact and generic-definition key groups first:
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. |
|
Measured allocation gains for #93 (median bytes/call; identical on .NET 8.0.31, 9.0.20 and 10.0.12):
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. |
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