Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Single constructor reflection optimisation #1325

Merged
merged 6 commits into from
Apr 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/Autofac/Core/Activators/InstanceActivator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,14 @@ protected void CheckNotDisposed()
{
if (IsDisposed)
{
throw new ObjectDisposedException(InstanceActivatorResources.InstanceActivatorDisposed, innerException: null);
// Call separate throw method to allow inlining of the disposal check.
ThrowDisposedException();
}
}

[DoesNotReturn]
private static void ThrowDisposedException()
{
throw new ObjectDisposedException(InstanceActivatorResources.InstanceActivatorDisposed, innerException: null);
}
}
15 changes: 12 additions & 3 deletions src/Autofac/Core/Activators/Reflection/BoundConstructor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,19 @@ public object Instantiate()
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, BoundConstructorResources.CannotInstantitate, Description));
}

var values = new object?[_valueRetrievers!.Length];
for (var i = 0; i < _valueRetrievers.Length; ++i)
object?[] values;

if (_valueRetrievers!.Length != 0)
{
values = new object?[_valueRetrievers!.Length];
for (var i = 0; i < _valueRetrievers.Length; ++i)
{
values[i] = _valueRetrievers[i]();
}
}
else
{
values[i] = _valueRetrievers[i]();
values = Array.Empty<object?>();
}

try
Expand Down
11 changes: 10 additions & 1 deletion src/Autofac/Core/Activators/Reflection/ConstructorBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public ConstructorBinder(ConstructorInfo constructorInfo)
if (_illegalParameter is null)
{
// Build the invoker.
_factory = FactoryCache.GetOrAdd(constructorInfo, FactoryBuilder);
_factory = FactoryCache.GetOrAdd(Constructor, FactoryBuilder);
}
}

Expand Down Expand Up @@ -113,6 +113,15 @@ public BoundConstructor Bind(IEnumerable<Parameter> availableParameters, ICompon
return BoundConstructor.ForBindSuccess(this, _factory!, valueRetrievers);
}

/// <summary>
/// Get the constructor factory delegate.
/// </summary>
/// <remarks>Will return null if the constructor contains an invalid parameter.</remarks>
internal Func<object?[], object>? GetConstructorInvoker()
{
return _factory;
}

private static Func<object?[], object> GetConstructorInvoker(ConstructorInfo constructorInfo)
{
var paramsInfo = constructorInfo.GetParameters();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) Autofac Project. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

namespace Autofac.Core.Activators.Reflection;

/// <summary>
/// Defines an interface that indicates a constructor selector can attempt to
/// determine the correct constructor early, as the container is built,
/// without needing to understand the set of parameters in each resolve.
/// </summary>
public interface IConstructorSelectorWithEarlyBinding : IConstructorSelector
{
/// <summary>
/// Given the set of all found constructors for a registration, try and
/// select the correct single <see cref="ConstructorBinder"/> to use.
/// </summary>
/// <param name="constructorBinders">
/// The set of binders for all constructors found by
/// <see cref="IConstructorFinder"/> on the registration.
/// </param>
/// <returns>
/// The single, correct binder to use. If the method returns null, this
/// indicates the selector requires resolve-time parameters, and the default
/// <see cref="IConstructorSelector.SelectConstructorBinding"/> method will
/// be invoked.
/// </returns>
ConstructorBinder? SelectConstructorBinder(ConstructorBinder[] constructorBinders);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace Autofac.Core.Activators.Reflection;
/// <summary>
/// Selects a constructor based on its signature.
/// </summary>
public class MatchingSignatureConstructorSelector : IConstructorSelector
public class MatchingSignatureConstructorSelector : IConstructorSelector, IConstructorSelectorWithEarlyBinding
{
private readonly Type[] _signature;

Expand Down Expand Up @@ -77,4 +77,49 @@ public BoundConstructor SelectConstructorBinding(BoundConstructor[] constructorB

throw new DependencyResolutionException(string.Format(CultureInfo.CurrentCulture, MatchingSignatureConstructorSelectorResources.TooManyConstructorsMatch, signature));
}

/// <summary>
/// Selects the best constructor from the available constructor bindings.
/// </summary>
/// <param name="constructorBinders">Available constructors.</param>
/// <returns>The best constructor.</returns>
public ConstructorBinder SelectConstructorBinder(ConstructorBinder[] constructorBinders)
{
if (constructorBinders == null)
{
throw new ArgumentNullException(nameof(constructorBinders));
}

var matchingCount = 0;
ConstructorBinder? chosen = null;

for (var idx = 0; idx < constructorBinders.Length; idx++)
{
var binding = constructorBinders[idx];

// Concievably could store the set of parameter types in the binder as well, but
// that's yet more memory up-front, for a less used constructor selector.
if (binding.Parameters.Select(p => p.ParameterType).SequenceEqual(_signature))
{
chosen = binding;
matchingCount++;
}
}

if (matchingCount == 1)
{
return chosen!;
}

// DeclaringType will be non-null for constructors.
var targetTypeName = constructorBinders[0].Constructor.DeclaringType!.Name;
var signature = string.Join(", ", _signature.Select(t => t.Name).ToArray());

if (matchingCount == 0)
{
throw new DependencyResolutionException(string.Format(CultureInfo.CurrentCulture, MatchingSignatureConstructorSelectorResources.RequiredConstructorNotAvailable, targetTypeName, signature));
}

throw new DependencyResolutionException(string.Format(CultureInfo.CurrentCulture, MatchingSignatureConstructorSelectorResources.TooManyConstructorsMatch, signature));
}
}
99 changes: 97 additions & 2 deletions src/Autofac/Core/Activators/Reflection/ReflectionActivator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@

using System.Globalization;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using Autofac.Core.Resolving;
using Autofac.Core.Resolving.Pipeline;

namespace Autofac.Core.Activators.Reflection;
Expand Down Expand Up @@ -91,6 +93,23 @@ public void ConfigurePipeline(IComponentRegistryServices componentRegistryServic
binders[idx] = new ConstructorBinder(availableConstructors[idx]);
}

if (binders.Length == 1)
{
UseSingleConstructorActivation(pipelineBuilder, binders[0]);
return;
}
else if (ConstructorSelector is IConstructorSelectorWithEarlyBinding earlyBindingSelector)
{
var matchedConstructor = earlyBindingSelector.SelectConstructorBinder(binders);

if (matchedConstructor is not null)
{
UseSingleConstructorActivation(pipelineBuilder, matchedConstructor);

return;
}
}

_constructorBinders = binders;

pipelineBuilder.Use(ToString(), PipelinePhase.Activation, MiddlewareInsertionMode.EndOfPhase, (ctxt, next) =>
Expand All @@ -101,6 +120,66 @@ public void ConfigurePipeline(IComponentRegistryServices componentRegistryServic
});
}

private void UseSingleConstructorActivation(IResolvePipelineBuilder pipelineBuilder, ConstructorBinder singleConstructor)
{
if (singleConstructor.ParameterCount == 0)
{
var constructorInvoker = singleConstructor.GetConstructorInvoker();

if (constructorInvoker is null)
{
// This is not going to happen, because there is only 1 constructor, that constructor has no parameters,
// so there are no conditions under which GetConstructorInvoker will return null in this path.
// Throw an error here just in case (and to satisfy nullability checks).
throw new NoConstructorsFoundException(_implementationType, string.Format(CultureInfo.CurrentCulture, ReflectionActivatorResources.NoConstructorsAvailable, _implementationType, ConstructorFinder));
}

// If there are no arguments to the constructor, bypass all argument binding and pre-bind the constructor.
var boundConstructor = BoundConstructor.ForBindSuccess(
singleConstructor,
constructorInvoker,
Array.Empty<Func<object?>>());

// Fast-path to just create an instance.
pipelineBuilder.Use(ToString(), PipelinePhase.Activation, MiddlewareInsertionMode.EndOfPhase, (ctxt, next) =>
{
CheckNotDisposed();

var instance = boundConstructor.Instantiate();

InjectProperties(instance, ctxt);

ctxt.Instance = instance;

next(ctxt);
});
}
else
{
pipelineBuilder.Use(ToString(), PipelinePhase.Activation, MiddlewareInsertionMode.EndOfPhase, (ctxt, next) =>
{
CheckNotDisposed();

var prioritisedParameters = GetAllParameters(ctxt.Parameters);

var bound = singleConstructor.Bind(prioritisedParameters, ctxt);

if (!bound.CanInstantiate)
{
throw new DependencyResolutionException(GetBindingFailureMessage(new[] { bound }));
}

var instance = bound.Instantiate();

InjectProperties(instance, ctxt);

ctxt.Instance = instance;

next(ctxt);
});
}
}

/// <summary>
/// Activate an instance in the provided context.
/// </summary>
Expand Down Expand Up @@ -146,8 +225,7 @@ private object ActivateInstance(IComponentContext context, IEnumerable<Parameter

private BoundConstructor[] GetAllBindings(ConstructorBinder[] availableConstructors, IComponentContext context, IEnumerable<Parameter> parameters)
{
// Most often, there will be no `parameters` and/or no `_defaultParameters`; in both of those cases we can avoid allocating.
var prioritisedParameters = parameters.Any() ? EnumerateParameters(parameters) : _defaultParameters;
var prioritisedParameters = GetAllParameters(parameters);

var boundConstructors = new BoundConstructor[availableConstructors.Length];
var validBindings = availableConstructors.Length;
Expand All @@ -172,6 +250,23 @@ private BoundConstructor[] GetAllBindings(ConstructorBinder[] availableConstruct
return boundConstructors;
}

private IEnumerable<Parameter> GetAllParameters(IEnumerable<Parameter> parameters)
{
// Most often, there will be no `parameters` and/or no `_defaultParameters`; in both of those cases we can avoid allocating.
// Do a reference compare with the NoParameters constant; faster than an Any() check for the common case.
if (ReferenceEquals(ResolveRequest.NoParameters, parameters))
{
return _defaultParameters;
}

if (parameters.Any())
{
return EnumerateParameters(parameters);
}

return _defaultParameters;
}

private IEnumerable<Parameter> EnumerateParameters(IEnumerable<Parameter> parameters)
{
foreach (var param in parameters)
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 8 additions & 1 deletion src/Autofac/Core/Lifetime/LifetimeScope.cs
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,8 @@ private void CheckNotDisposed()
{
if (IsTreeDisposed())
{
throw new ObjectDisposedException(LifetimeScopeResources.ScopeIsDisposed, innerException: null);
// Throw in a separate method to allow this check to be inlined.
ThrowDisposedException();
}
}

Expand Down Expand Up @@ -490,4 +491,10 @@ private bool IsTreeDisposed()
/// Fired when a resolve operation is beginning in this scope.
/// </summary>
public event EventHandler<ResolveOperationBeginningEventArgs>? ResolveOperationBeginning;

[DoesNotReturn]
private static void ThrowDisposedException()
{
throw new ObjectDisposedException(LifetimeScopeResources.ScopeIsDisposed, innerException: null);
}
}
Loading