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
Memory traffic optimization #919
Conversation
Replaced some Collection interfaces with specific type declarations up to 30% reduction of memory allocation traffic during registration
Thanks for the PR! There is a lot to unpack here so it will take some time. I don't think we'll be able to pull the whole thing as-is because in some cases it changes the public API and will cause consumers to break. We will likely have to find a balance of what we can optimize vs. what we can't based on how breaking it will be. There are definitely a few free ones we can easily take. |
Thanks for the PR @Belorus. There are some great optimizations here. I agree with @tillig that we need to figure out what can come straight in and what might have to wait for a I've wondered for a while now how much time would be saved at resolve time (particularly the first call that initializes the We have a benchmarks project that might be a good way to quantify just how much benefit some of the changes bring. In the past BenchmarkDotNet had the I will try to take a first pass over the review and see if things can be flagged for immediate inclusion. I'm very excited to see a PR that is focused on performance. |
I just noticed that the PR targets |
Thanks for the review guys! |
I'm doing a quick pass through now and adding a few comments that might help. There are a few changes that are technically breaking that I would probably be fine with. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great overall. Just have to make some decisions on what "technically" breaking changes we are cool with.
/// Prevents inclusion of standard modules like support for | ||
/// relationship types including <see cref="IEnumerable{T}"/> etc. | ||
/// </summary> | ||
ExcludeDefaultModules = 2, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a breaking change. We could add an overload to Build
takes the DefaultServiceFlags
and passes through None
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, an overload would be better.
/// </summary> | ||
/// <param name="service">Service types to expose.</param> | ||
/// <returns>A registration builder allowing further configuration of the component.</returns> | ||
IRegistrationBuilder<TLimit, TActivatorData, TRegistrationStyle> As(Type service); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like these are to prevent the need to call the params
method with the array allocation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding this is technically a breaking change. Super low risk, but breaking. Curious if adding this would be a problem during an upgrade where custom extension methods (that people have written) indirectly call this and the compiler will get confused as to which method it should be calling. We had some problems before adding optional parameters to an existing public API method where it was seen as breaking in odd and unfortunate ways.
@@ -108,11 +108,11 @@ public IConstructorSelector ConstructorSelector | |||
/// <summary> | |||
/// Gets the explicitly bound constructor parameters. | |||
/// </summary> | |||
public IList<Parameter> ConfiguredParameters { get; } = new List<Parameter>(); | |||
public List<Parameter> ConfiguredParameters { get; } = new List<Parameter>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this would cause any issues from the API perspective.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, likely not a big deal. Unclear how much this stuff gets used, if ever, outside of here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm trying to put this together with the change from Autofac.Core.Activators.Reflection.ReflectionActivator
. It looks like the change here wants to expose these as List<T>
. These appear to be primarily used when instantiating a ReflectionActivator
. The change in ReflectionActivator
is to stop taking an IEnumerable<T>
and take an IList<T>
instead.
Could we leave this alone (as IList<T>
) and only make the change in ReflectionActivator
? As it stands there's still going to be some implicit casting going on, which I'm guessing is the point of this.
@@ -149,7 +149,7 @@ public static class RegistrationBuilder | |||
Guid id, | |||
RegistrationData data, | |||
IInstanceActivator activator, | |||
IEnumerable<Service> services) | |||
Service[] services) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are definitely breaking changes but I don't think these methods would be used directly by consumers very often.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. Pretty low risk.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I brought all the changes from this file into the develop
branch.
@@ -179,11 +179,15 @@ public static class RegistrationBuilder | |||
var limitType = activator.LimitType; | |||
if (limitType != typeof(object)) | |||
{ | |||
foreach (var ts in services.OfType<IServiceWithType>()) | |||
foreach (var ts in services) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All good.
@@ -56,20 +56,21 @@ public class ComponentRegistration : Disposable, IComponentRegistration | |||
IComponentLifetime lifetime, | |||
InstanceSharing sharing, | |||
InstanceOwnership ownership, | |||
IEnumerable<Service> services, | |||
Service[] services, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Breaking change but also probably not something called directly too often.
.Where(si => si.Value.ShouldRecalculateAdaptersOn(registration)) | ||
.Select(si => si.Key) | ||
.ToArray(); | ||
List<Service> adapterServices = new List<Service>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good
.Concat(_sourceImplementations) | ||
.Concat(_preserveDefaultImplementations); | ||
|
||
var resultingCollection = Enumerable.Reverse(_defaultImplementations); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Everything in this file looks good.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I brought all the changes from this file into the develop
branch.
@@ -61,8 +61,11 @@ public static void CheckForCircularDependency(IComponentRegistration registratio | |||
throw new DependencyResolutionException(string.Format(CultureInfo.CurrentCulture, CircularDependencyDetectorResources.MaxDepthExceeded, registration)); | |||
|
|||
// Checks for circular dependency | |||
if (activationStack.Any(a => a.ComponentRegistration == registration)) | |||
throw new DependencyResolutionException(string.Format(CultureInfo.CurrentCulture, CircularDependencyDetectorResources.CircularDependency, CreateDependencyGraphTo(registration, activationStack))); | |||
foreach (var a in activationStack) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I brought all the changes from this file into the develop
branch.
{ | ||
var parameter = new ResolvedParameter( | ||
(pi, c) => pi.ParameterType == decoratedParameterType, | ||
(pi, c) => c.ResolveComponent(decoratedComponent, Enumerable.Empty<Parameter>())); | ||
|
||
return new[] { parameter }.Concat(configuredParameters); | ||
var resultArray = new Parameter[configuredParameters.Count + 1]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No problem
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I brought all the changes from this file into the develop
branch.
I've added some comments in a review @tillig. Would be interested to see if you agree on some of the changes that are technically breaking but very unlikely to have been used directly. |
@@ -4,7 +4,7 @@ | |||
<Description>Autofac is an IoC container for Microsoft .NET. It manages the dependencies between classes so that applications stay easy to change as they grow in size and complexity.</Description> | |||
<VersionPrefix>4.8.1</VersionPrefix> | |||
<TargetFrameworks>netstandard1.1;net45</TargetFrameworks> | |||
<NoWarn>$(NoWarn);CS1591</NoWarn> | |||
<NoWarn>$(NoWarn);CS1591;CA1819</NoWarn> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CA1819 indicates that public/protected properties shouldn't return arrays. The reason they note is that if the property doesn't return a copy of the array then people can (and likely will) start using that property as an indexed property to start poking data into places they possibly shouldn't.
I'm not a big stickler for stuff like this, but I feel like there's a decent point here - where we may have public properties (like in IComponentRegistration
) that were returning IEnumerable<T>
and the switch to T[]
enables people to do things they maybe shouldn't be doing. I've read too many Raymond Chen posts where people write programs using APIs that were never intended for a particular purpose, but now it's all locked in.
I'd recommend not ignoring this and, instead, being very thoughtful about switching away from an interface and using [SuppressMessage]
to pinpoint the specific choices and document why that choice was made.
From a larger perspective, moving from IList<T>
to List<T>
, from IEnumerable<T>
to T[]
... that stuff may limit our ability to change underlying implementations. For example, if we need to store things in a SortedList<T>
or start using Span<T>
in certain places, switching from an interface to a concrete type makes that implementation detail a breaking change.
@@ -147,17 +147,17 @@ public IComponentLifetime Lifetime | |||
/// <summary> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The switch from interface to concrete implementation here is one of the areas I'm a little concerned about. Exposing the set of handlers like this makes any underlying improvement a breaking change.
I just made a pass @alexmg and added comments. I think most of the changes here are great. I am slightly concerned about the exposure of concrete backing types in public APIs (a la CA1819) though I recognize there is performance hit when using interfaces over concrete types. I want perf, but I don't want to lose sight of the fact that the code in the public APIs is more an agreement between Autofac and the consumer and not 100% "just Autofac code." I'm guessing the .NET BCL could also add perf improvements by switching to concrete types in places instead of interfaces, but that's not an option because it's written as a public API, not as app code owned entirely by the app developer. |
…ricDecoratorRegistrationSource.
I took some time and brought in some, but not all, of the changes here. From a public interface perspective, I really am concerned that using arrays in public properties may open doors to people shooting themselves in the foot by trying to change values in the array. Like, "I know, let's swap the order of this and that because reasons!" It sounds silly, but we do see a lot of this sort of thing come through on StackOverflow and other venues. I'll mark the things I brought in. |
@@ -60,7 +60,7 @@ public IEnumerable<IComponentRegistration> RegistrationsFor(Service service, Fun | |||
if (registrationAccessor == null) throw new ArgumentNullException(nameof(registrationAccessor)); | |||
|
|||
Type constructedImplementationType; | |||
IEnumerable<Service> services; | |||
Service[] services; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I brought all the changes from this file into the develop
branch.
@@ -41,7 +41,7 @@ internal static class OpenGenericServiceBinder | |||
IEnumerable<Service> configuredOpenGenericServices, | |||
Type openGenericImplementationType, | |||
out Type constructedImplementationType, | |||
out IEnumerable<Service> constructedServices) | |||
out Service[] constructedServices) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I brought all the changes from this file into the develop
branch.
I think there are still things we may be able to pull in here for the Autofac 5 release, but I'm not sure we can really pull it in as-is anymore. Given that, I'm going to close the PR and tag @autofac/autofac so we can refer to it later when doing additional refactoring. |
Hi!
I work on mobile games written in C#/Xamarin, and start time is a bit critical there.
To improve it a bit, i slightly optimized Autofac memory allocations during registration phase to get rid of some simple ones:
Also i introduced a bit more fine-grained control of default services.
In my codebase with 1600 registrations results are following:
4.8.1: 122K allocations (3.45MB), 80K collected (2.2MB)
4.8.1+PR: 82K allocations (2.25MB), 46K collected (1.20MB)
Speedup was about ~10% on .NET 4.6 (No sample, made 10 runs with logging of Stopwatch and compared best timings).
We use this fork for over a year, and i though someone else could find it useful.
TO BE UPDATED: I'll add some runtime optimizations (Resolve-time)