Pre-size DI scope resolved-services dictionary (capped at 36)#128630
Pre-size DI scope resolved-services dictionary (capped at 36)#128630Copilot wants to merge 4 commits into
Conversation
Co-authored-by: rosebyte <14963300+rosebyte@users.noreply.github.com>
|
@EgorBot -intel -amd --filter "Microsoft.Extensions.DependencyInjection.GetService*" |
|
@EgorBot -intel -amd using System;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using Microsoft.Extensions.DependencyInjection;
BenchmarkSwitcher.FromAssembly(typeof(ScopeBench).Assembly).Run(args);
[MemoryDiagnoser]
public class ScopeBench
{
private IServiceProvider _provider = default!;
private Type[] _resolveTypes = default!;
[Params(20, 50, 100, 200)]
public int Registered;
[Params(5, 20, 50)]
public int Resolutions;
[GlobalSetup]
public void Setup()
{
Type[] types = EmitTypes(Registered);
var services = new ServiceCollection();
foreach (Type t in types)
services.AddScoped(t);
_provider = services.BuildServiceProvider();
_resolveTypes = types.Take(Math.Min(Resolutions, Registered)).ToArray();
}
[Benchmark]
public void CreateScopeAndResolve()
{
using IServiceScope scope = _provider.CreateScope();
IServiceProvider sp = scope.ServiceProvider;
for (int i = 0; i < _resolveTypes.Length; i++)
sp.GetService(_resolveTypes[i]);
}
private static Type[] EmitTypes(int n)
{
AssemblyBuilder asm = AssemblyBuilder.DefineDynamicAssembly(
new AssemblyName("ScopeBench.Svcs"), AssemblyBuilderAccess.Run);
ModuleBuilder mod = asm.DefineDynamicModule("m");
var types = new Type[n];
for (int i = 0; i < n; i++)
{
TypeBuilder tb = mod.DefineType(
$"Svc{i}", TypeAttributes.Public | TypeAttributes.Class);
types[i] = tb.CreateType()!;
}
return types;
}
} |
There was a problem hiding this comment.
Pull request overview
This PR adjusts Microsoft.Extensions.DependencyInjection scope initialization to reduce ResolvedServices dictionary growth by pre-sizing child-scope caches based on the number of scoped registrations in the container.
Changes:
- Compute and store an internal
ResolvedServicesCapacityinServiceProviderbased on the count of scopedServiceDescriptors. - Initialize
ServiceProviderEngineScope.ResolvedServiceswith that capacity for non-root scopes (root scope remains at default sizing). - Add tests to validate child-scope pre-sizing and root-scope default sizing.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceProvider.cs | Adds ResolvedServicesCapacity and logic to compute it from scoped registrations during provider construction. |
| src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs | Uses ResolvedServicesCapacity to pre-size ResolvedServices for child scopes. |
| src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderEngineScopeTests.cs | Adds coverage asserting child vs. root scope dictionary sizing behavior. |
| public ServiceProviderEngineScope(ServiceProvider provider, bool isRootScope) | ||
| { | ||
| ResolvedServices = new Dictionary<ServiceCacheKey, object?>(); | ||
| ResolvedServices = new Dictionary<ServiceCacheKey, object?>(isRootScope ? 0 : provider.ResolvedServicesCapacity); | ||
| RootProvider = provider; | ||
| IsRootScope = isRootScope; |
| var provider = new ServiceProvider(services, ServiceProviderOptions.Default); | ||
| var scope = new ServiceProviderEngineScope(provider, isRootScope: false); | ||
|
|
||
| Assert.Equal(new Dictionary<ServiceCacheKey, object?>(2).EnsureCapacity(0), scope.ResolvedServices.EnsureCapacity(0)); | ||
| } |
|
@copilot , The benchmark results show the implementation is missing the cap that bounds per-scope over-allocation, so the patch regresses on sparse-resolution scopes (e.g. Registered=200, Resolutions=5 allocates ~13× more than main). Please apply the following changes.
In src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceProvider.cs, change the method to: private static int CalculateResolvedServicesCapacity(ICollection serviceDescriptors) The constant 36 lands on prime tier 37 via HashHelpers.GetPrime, bounding worst-case live allocation per scope to ~1.6 KB. This caps the regression observed on sparse-resolution scopes while still skipping the 3 → 7 → 17 resize sequence that dominates the garbage volume on main.
./build.sh clr+libs -rc release All tests must pass. |
…lation Co-authored-by: rosebyte <14963300+rosebyte@users.noreply.github.com>
Done. Changes in the latest commit:
|
|
@EgorBot -intel -amd using System;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using Microsoft.Extensions.DependencyInjection;
BenchmarkSwitcher.FromAssembly(typeof(ScopeBench).Assembly).Run(args);
[MemoryDiagnoser]
public class ScopeBench
{
private IServiceProvider _provider = default!;
private Type[] _resolveTypes = default!;
[Params(20, 50, 100, 200)]
public int Registered;
[Params(5, 20, 50)]
public int Resolutions;
[GlobalSetup]
public void Setup()
{
Type[] types = EmitTypes(Registered);
var services = new ServiceCollection();
foreach (Type t in types)
services.AddScoped(t);
_provider = services.BuildServiceProvider();
_resolveTypes = types.Take(Math.Min(Resolutions, Registered)).ToArray();
}
[Benchmark]
public void CreateScopeAndResolve()
{
using IServiceScope scope = _provider.CreateScope();
IServiceProvider sp = scope.ServiceProvider;
for (int i = 0; i < _resolveTypes.Length; i++)
sp.GetService(_resolveTypes[i]);
}
private static Type[] EmitTypes(int n)
{
AssemblyBuilder asm = AssemblyBuilder.DefineDynamicAssembly(
new AssemblyName("ScopeBench.Svcs"), AssemblyBuilderAccess.Run);
ModuleBuilder mod = asm.DefineDynamicModule("m");
var types = new Type[n];
for (int i = 0; i < n; i++)
{
TypeBuilder tb = mod.DefineType(
$"Svc{i}", TypeAttributes.Public | TypeAttributes.Class);
types[i] = tb.CreateType()!;
}
return types;
}
} |
DI scope resolution can spend a noticeable amount of allocation budget growing the per-scope
ResolvedServicesdictionary as scoped services are added. This change reduces those early resizes by sizing the dictionary from the container's scoped registrations instead of always starting from the default capacity, while capping the pre-allocated capacity to avoid regressions on sparse-resolution scopes.Capacity selection
ServiceProvider.Math.Min(scopedServices, 36)), which lands on prime tier 37 viaHashHelpers.GetPrime, bounding worst-case live allocation per scope to ~1.6 KB.ResolvedServices.Scope behavior
Coverage