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

Memory traffic optimization #919

Closed
wants to merge 4 commits into from
Closed

Memory traffic optimization #919

wants to merge 4 commits into from

Conversation

Belorus
Copy link

@Belorus Belorus commented May 30, 2018

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:

  1. foreach over interface (boxing of struct iterator)
  2. allocation of iterators inside LINQ calls
  3. allocation of delegates for LINQ calls

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)

Replaced some Collection interfaces with specific type declarations
up to 30% reduction of memory allocation traffic during registration
@tillig
Copy link
Member

tillig commented May 30, 2018

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.

@alexmg
Copy link
Member

alexmg commented Jun 5, 2018

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 5.0 release when breaking changes are to be expected. There are a few places that are technically breaking changes because a class is public but the likelihood of someone using it directly are very low.

I've wondered for a while now how much time would be saved at resolve time (particularly the first call that initializes the ServiceRegistrationInfo) by removing IRegistrationSources that are not required.

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 MemoryDiagnoser enabled by default but that was changed in a later version so the GC and memory allocation information isn't currently available but could be added back easily.

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.

@alexmg
Copy link
Member

alexmg commented Jun 5, 2018

I just noticed that the PR targets master directly as well. They would need to come into the develop branch first. Let's wait and see how the slicing goes before doing anything though.

@Belorus
Copy link
Author

Belorus commented Jun 5, 2018

Thanks for the review guys!
Then let me split this PR into 2 parts. One nonbreaking and second breaking. And I'll rebase it on develop.

@alexmg
Copy link
Member

alexmg commented Jun 5, 2018

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.

Copy link
Member

@alexmg alexmg left a 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,
Copy link
Member

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.

Copy link
Member

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);
Copy link
Member

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.

Copy link
Member

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>();
Copy link
Member

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.

Copy link
Member

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.

Copy link
Member

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)
Copy link
Member

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Pretty low risk.

Copy link
Member

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)
Copy link
Member

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,
Copy link
Member

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>();
Copy link
Member

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);
Copy link
Member

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.

Copy link
Member

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice

Copy link
Member

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];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem

Copy link
Member

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.

@alexmg
Copy link
Member

alexmg commented Jun 5, 2018

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>
Copy link
Member

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>
Copy link
Member

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.

@tillig
Copy link
Member

tillig commented Jun 5, 2018

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.

tillig added a commit that referenced this pull request Aug 16, 2018
tillig added a commit that referenced this pull request Aug 16, 2018
tillig added a commit that referenced this pull request Aug 16, 2018
tillig added a commit that referenced this pull request Aug 16, 2018
@tillig
Copy link
Member

tillig commented Aug 16, 2018

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;
Copy link
Member

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)
Copy link
Member

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.

@tillig
Copy link
Member

tillig commented Nov 20, 2019

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants