Skip to content

Commit 00937b2

Browse files
authored
Improve perf of ActivatorUtilities.CreateInstance() (#91290)
1 parent 38445c3 commit 00937b2

File tree

5 files changed

+372
-53
lines changed

5 files changed

+372
-53
lines changed

src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/ActivatorUtilities.cs

+176-51
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
5+
using System.Collections.Concurrent;
56
using System.Diagnostics;
67
using System.Diagnostics.CodeAnalysis;
78
using System.Linq.Expressions;
@@ -10,13 +11,25 @@
1011
using System.Runtime.ExceptionServices;
1112
using Microsoft.Extensions.Internal;
1213

14+
#if NETCOREAPP
15+
[assembly: System.Reflection.Metadata.MetadataUpdateHandler(typeof(Microsoft.Extensions.DependencyInjection.ActivatorUtilities.ActivatorUtilitiesUpdateHandler))]
16+
#endif
17+
1318
namespace Microsoft.Extensions.DependencyInjection
1419
{
1520
/// <summary>
1621
/// Helper code for the various activator services.
1722
/// </summary>
1823
public static class ActivatorUtilities
1924
{
25+
#if NETCOREAPP
26+
// Support caching of constructor metadata for the common case of types in non-collectible assemblies.
27+
private static readonly ConcurrentDictionary<Type, ConstructorInfoEx[]> s_constructorInfos = new();
28+
29+
// Support caching of constructor metadata for types in collectible assemblies.
30+
private static readonly Lazy<ConditionalWeakTable<Type, ConstructorInfoEx[]>> s_collectibleConstructorInfos = new();
31+
#endif
32+
2033
#if NET8_0_OR_GREATER
2134
// Maximum number of fixed arguments for ConstructorInvoker.Invoke(arg1, etc).
2235
private const int FixedArgumentThreshold = 4;
@@ -47,6 +60,17 @@ public static object CreateInstance(
4760
throw new InvalidOperationException(SR.CannotCreateAbstractClasses);
4861
}
4962

63+
ConstructorInfoEx[]? constructors;
64+
#if NETCOREAPP
65+
if (!s_constructorInfos.TryGetValue(instanceType, out constructors))
66+
{
67+
constructors = GetOrAddConstructors(instanceType);
68+
}
69+
#else
70+
constructors = CreateConstructorInfoExs(instanceType);
71+
#endif
72+
73+
ConstructorInfoEx? constructor;
5074
IServiceProviderIsService? serviceProviderIsService = provider.GetService<IServiceProviderIsService>();
5175
// if container supports using IServiceProviderIsService, we try to find the longest ctor that
5276
// (a) matches all parameters given to CreateInstance
@@ -61,10 +85,11 @@ public static object CreateInstance(
6185
ConstructorMatcher bestMatcher = default;
6286
bool multipleBestLengthFound = false;
6387

64-
foreach (ConstructorInfo? constructor in instanceType.GetConstructors())
88+
for (int i = 0; i < constructors.Length; i++)
6589
{
66-
var matcher = new ConstructorMatcher(constructor);
67-
bool isPreferred = constructor.IsDefined(typeof(ActivatorUtilitiesConstructorAttribute), false);
90+
constructor = constructors[i];
91+
ConstructorMatcher matcher = new(constructor);
92+
bool isPreferred = constructor.IsPreferred;
6893
int length = matcher.Match(parameters, serviceProviderIsService);
6994

7095
if (isPreferred)
@@ -105,18 +130,79 @@ public static object CreateInstance(
105130
}
106131
}
107132

108-
Type?[] argumentTypes = new Type[parameters.Length];
109-
for (int i = 0; i < argumentTypes.Length; i++)
133+
Type?[] argumentTypes;
134+
if (parameters.Length == 0)
110135
{
111-
argumentTypes[i] = parameters[i]?.GetType();
136+
argumentTypes = Type.EmptyTypes;
137+
}
138+
else
139+
{
140+
argumentTypes = new Type[parameters.Length];
141+
for (int i = 0; i < argumentTypes.Length; i++)
142+
{
143+
argumentTypes[i] = parameters[i]?.GetType();
144+
}
112145
}
113146

114147
FindApplicableConstructor(instanceType, argumentTypes, out ConstructorInfo constructorInfo, out int?[] parameterMap);
115-
var constructorMatcher = new ConstructorMatcher(constructorInfo);
148+
149+
// Find the ConstructorInfoEx from the given constructorInfo.
150+
constructor = null;
151+
foreach (ConstructorInfoEx ctor in constructors)
152+
{
153+
if (ReferenceEquals(ctor.Info, constructorInfo))
154+
{
155+
constructor = ctor;
156+
break;
157+
}
158+
}
159+
160+
Debug.Assert(constructor != null);
161+
162+
var constructorMatcher = new ConstructorMatcher(constructor);
116163
constructorMatcher.MapParameters(parameterMap, parameters);
117164
return constructorMatcher.CreateInstance(provider);
118165
}
119166

167+
#if NETCOREAPP
168+
private static ConstructorInfoEx[] GetOrAddConstructors(
169+
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type)
170+
{
171+
// Not found. Do the slower work of checking for the value in the correct cache.
172+
// Null and non-collectible load contexts use the default cache.
173+
if (!type.Assembly.IsCollectible)
174+
{
175+
return s_constructorInfos.GetOrAdd(type, CreateConstructorInfoExs(type));
176+
}
177+
178+
// Collectible load contexts should use the ConditionalWeakTable so they can be unloaded.
179+
if (s_collectibleConstructorInfos.Value.TryGetValue(type, out ConstructorInfoEx[]? value))
180+
{
181+
return value;
182+
}
183+
184+
value = CreateConstructorInfoExs(type);
185+
186+
// ConditionalWeakTable doesn't support GetOrAdd() so use AddOrUpdate(). This means threads
187+
// can have different instances for the same type, but that is OK since they are equivalent.
188+
s_collectibleConstructorInfos.Value.AddOrUpdate(type, value);
189+
return value;
190+
}
191+
#endif // NETCOREAPP
192+
193+
private static ConstructorInfoEx[] CreateConstructorInfoExs(
194+
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type)
195+
{
196+
ConstructorInfo[] constructors = type.GetConstructors();
197+
ConstructorInfoEx[]? value = new ConstructorInfoEx[constructors.Length];
198+
for (int i = 0; i < constructors.Length; i++)
199+
{
200+
value[i] = new ConstructorInfoEx(constructors[i]);
201+
}
202+
203+
return value;
204+
}
205+
120206
/// <summary>
121207
/// Create a delegate that will instantiate a type with constructor arguments provided directly
122208
/// and/or from an <see cref="IServiceProvider"/>.
@@ -551,58 +637,82 @@ private static bool TryCreateParameterMap(ParameterInfo[] constructorParameters,
551637
return true;
552638
}
553639

554-
private static object? GetService(IServiceProvider serviceProvider, ParameterInfo parameterInfo)
640+
private sealed class ConstructorInfoEx
555641
{
556-
// Handle keyed service
557-
if (TryGetServiceKey(parameterInfo, out object? key))
642+
public readonly ConstructorInfo Info;
643+
public readonly ParameterInfo[] Parameters;
644+
public readonly bool IsPreferred;
645+
private readonly object?[]? _parameterKeys;
646+
647+
public ConstructorInfoEx(ConstructorInfo constructor)
558648
{
559-
if (serviceProvider is IKeyedServiceProvider keyedServiceProvider)
649+
Info = constructor;
650+
Parameters = constructor.GetParameters();
651+
IsPreferred = constructor.IsDefined(typeof(ActivatorUtilitiesConstructorAttribute), inherit: false);
652+
653+
for (int i = 0; i < Parameters.Length; i++)
560654
{
561-
return keyedServiceProvider.GetKeyedService(parameterInfo.ParameterType, key);
655+
FromKeyedServicesAttribute? attr = (FromKeyedServicesAttribute?)
656+
Attribute.GetCustomAttribute(Parameters[i], typeof(FromKeyedServicesAttribute), inherit: false);
657+
658+
if (attr is not null)
659+
{
660+
_parameterKeys ??= new object?[Parameters.Length];
661+
_parameterKeys[i] = attr.Key;
662+
}
562663
}
563-
throw new InvalidOperationException(SR.KeyedServicesNotSupported);
564664
}
565-
// Try non keyed service
566-
return serviceProvider.GetService(parameterInfo.ParameterType);
567-
}
568665

569-
private static bool IsService(IServiceProviderIsService serviceProviderIsService, ParameterInfo parameterInfo)
570-
{
571-
// Handle keyed service
572-
if (TryGetServiceKey(parameterInfo, out object? key))
666+
public bool IsService(IServiceProviderIsService serviceProviderIsService, int parameterIndex)
573667
{
574-
if (serviceProviderIsService is IServiceProviderIsKeyedService serviceProviderIsKeyedService)
668+
ParameterInfo parameterInfo = Parameters[parameterIndex];
669+
670+
// Handle keyed service
671+
object? key = _parameterKeys?[parameterIndex];
672+
if (key is not null)
575673
{
576-
return serviceProviderIsKeyedService.IsKeyedService(parameterInfo.ParameterType, key);
674+
if (serviceProviderIsService is IServiceProviderIsKeyedService serviceProviderIsKeyedService)
675+
{
676+
return serviceProviderIsKeyedService.IsKeyedService(parameterInfo.ParameterType, key);
677+
}
678+
679+
throw new InvalidOperationException(SR.KeyedServicesNotSupported);
577680
}
578-
throw new InvalidOperationException(SR.KeyedServicesNotSupported);
681+
682+
// Use non-keyed service
683+
return serviceProviderIsService.IsService(parameterInfo.ParameterType);
579684
}
580-
// Try non keyed service
581-
return serviceProviderIsService.IsService(parameterInfo.ParameterType);
582-
}
583685

584-
private static bool TryGetServiceKey(ParameterInfo parameterInfo, out object? key)
585-
{
586-
foreach (var attribute in parameterInfo.GetCustomAttributes<FromKeyedServicesAttribute>(false))
686+
public object? GetService(IServiceProvider serviceProvider, int parameterIndex)
587687
{
588-
key = attribute.Key;
589-
return true;
688+
ParameterInfo parameterInfo = Parameters[parameterIndex];
689+
690+
// Handle keyed service
691+
object? key = _parameterKeys?[parameterIndex];
692+
if (key is not null)
693+
{
694+
if (serviceProvider is IKeyedServiceProvider keyedServiceProvider)
695+
{
696+
return keyedServiceProvider.GetKeyedService(parameterInfo.ParameterType, key);
697+
}
698+
699+
throw new InvalidOperationException(SR.KeyedServicesNotSupported);
700+
}
701+
702+
// Use non-keyed service
703+
return serviceProvider.GetService(parameterInfo.ParameterType);
590704
}
591-
key = null;
592-
return false;
593705
}
594706

595707
private readonly struct ConstructorMatcher
596708
{
597-
private readonly ConstructorInfo _constructor;
598-
private readonly ParameterInfo[] _parameters;
709+
private readonly ConstructorInfoEx _constructor;
599710
private readonly object?[] _parameterValues;
600711

601-
public ConstructorMatcher(ConstructorInfo constructor)
712+
public ConstructorMatcher(ConstructorInfoEx constructor)
602713
{
603714
_constructor = constructor;
604-
_parameters = _constructor.GetParameters();
605-
_parameterValues = new object?[_parameters.Length];
715+
_parameterValues = new object[constructor.Parameters.Length];
606716
}
607717

608718
public int Match(object[] givenParameters, IServiceProviderIsService serviceProviderIsService)
@@ -612,10 +722,10 @@ public int Match(object[] givenParameters, IServiceProviderIsService serviceProv
612722
Type? givenType = givenParameters[givenIndex]?.GetType();
613723
bool givenMatched = false;
614724

615-
for (int applyIndex = 0; applyIndex < _parameters.Length; applyIndex++)
725+
for (int applyIndex = 0; applyIndex < _constructor.Parameters.Length; applyIndex++)
616726
{
617727
if (_parameterValues[applyIndex] == null &&
618-
_parameters[applyIndex].ParameterType.IsAssignableFrom(givenType))
728+
_constructor.Parameters[applyIndex].ParameterType.IsAssignableFrom(givenType))
619729
{
620730
givenMatched = true;
621731
_parameterValues[applyIndex] = givenParameters[givenIndex];
@@ -630,12 +740,12 @@ public int Match(object[] givenParameters, IServiceProviderIsService serviceProv
630740
}
631741

632742
// confirms the rest of ctor arguments match either as a parameter with a default value or as a service registered
633-
for (int i = 0; i < _parameters.Length; i++)
743+
for (int i = 0; i < _constructor.Parameters.Length; i++)
634744
{
635745
if (_parameterValues[i] == null &&
636-
!IsService(serviceProviderIsService, _parameters[i]))
746+
!_constructor.IsService(serviceProviderIsService, i))
637747
{
638-
if (ParameterDefaultValue.TryGetDefaultValue(_parameters[i], out object? defaultValue))
748+
if (ParameterDefaultValue.TryGetDefaultValue(_constructor.Parameters[i], out object? defaultValue))
639749
{
640750
_parameterValues[i] = defaultValue;
641751
}
@@ -646,21 +756,21 @@ public int Match(object[] givenParameters, IServiceProviderIsService serviceProv
646756
}
647757
}
648758

649-
return _parameters.Length;
759+
return _constructor.Parameters.Length;
650760
}
651761

652762
public object CreateInstance(IServiceProvider provider)
653763
{
654-
for (int index = 0; index < _parameters.Length; index++)
764+
for (int index = 0; index < _constructor.Parameters.Length; index++)
655765
{
656766
if (_parameterValues[index] == null)
657767
{
658-
object? value = GetService(provider, _parameters[index]);
768+
object? value = _constructor.GetService(provider, index);
659769
if (value == null)
660770
{
661-
if (!ParameterDefaultValue.TryGetDefaultValue(_parameters[index], out object? defaultValue))
771+
if (!ParameterDefaultValue.TryGetDefaultValue(_constructor.Parameters[index], out object? defaultValue))
662772
{
663-
throw new InvalidOperationException(SR.Format(SR.UnableToResolveService, _parameters[index].ParameterType, _constructor.DeclaringType));
773+
throw new InvalidOperationException(SR.Format(SR.UnableToResolveService, _constructor.Parameters[index].ParameterType, _constructor.Info.DeclaringType));
664774
}
665775
else
666776
{
@@ -677,7 +787,7 @@ public object CreateInstance(IServiceProvider provider)
677787
#if NETFRAMEWORK || NETSTANDARD2_0
678788
try
679789
{
680-
return _constructor.Invoke(_parameterValues);
790+
return _constructor.Info.Invoke(_parameterValues);
681791
}
682792
catch (TargetInvocationException ex) when (ex.InnerException != null)
683793
{
@@ -686,13 +796,13 @@ public object CreateInstance(IServiceProvider provider)
686796
throw;
687797
}
688798
#else
689-
return _constructor.Invoke(BindingFlags.DoNotWrapExceptions, binder: null, parameters: _parameterValues, culture: null);
799+
return _constructor.Info.Invoke(BindingFlags.DoNotWrapExceptions, binder: null, parameters: _parameterValues, culture: null);
690800
#endif
691801
}
692802

693803
public void MapParameters(int?[] parameterMap, object[] givenParameters)
694804
{
695-
for (int i = 0; i < _parameters.Length; i++)
805+
for (int i = 0; i < _constructor.Parameters.Length; i++)
696806
{
697807
if (parameterMap[i] != null)
698808
{
@@ -974,5 +1084,20 @@ private static object ReflectionFactoryCanonical(
9741084
return constructor.Invoke(BindingFlags.DoNotWrapExceptions, binder: null, constructorArguments, culture: null);
9751085
}
9761086
#endif // NET8_0_OR_GREATER
1087+
1088+
#if NETCOREAPP
1089+
internal static class ActivatorUtilitiesUpdateHandler
1090+
{
1091+
public static void ClearCache(Type[]? _)
1092+
{
1093+
// Ignore the Type[] argument; just clear the caches.
1094+
s_constructorInfos.Clear();
1095+
if (s_collectibleConstructorInfos.IsValueCreated)
1096+
{
1097+
s_collectibleConstructorInfos.Value.Clear();
1098+
}
1099+
}
1100+
}
1101+
#endif
9771102
}
9781103
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.Extensions.DependencyInjection;
5+
6+
namespace CollectibleAssembly
7+
{
8+
public class ClassToCreate
9+
{
10+
public object ClassAsCtorArgument { get; set; }
11+
12+
public ClassToCreate(ClassAsCtorArgument obj) { ClassAsCtorArgument = obj; }
13+
14+
public static object Create(ServiceProvider provider)
15+
{
16+
// Both the type to create (ClassToCreate) and the ctor's arg type (ClassAsCtorArgument) are
17+
// located in this assembly, so both types need to be GC'd for this assembly to be collected.
18+
return ActivatorUtilities.CreateInstance<ClassToCreate>(provider, new ClassAsCtorArgument());
19+
}
20+
}
21+
22+
public class ClassAsCtorArgument
23+
{
24+
}
25+
}

0 commit comments

Comments
 (0)