-
Notifications
You must be signed in to change notification settings - Fork 22
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
improve performance and reduce allocations #40
base: master
Are you sure you want to change the base?
Conversation
Sorry @bollhals for the late reply. But the problem for us is, that we just took over BoDi and aren't yet familiar with the codebase to a degree, were we are a little bit hesitant to merge it. |
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.
Hi,
unfortunately I also get the System.Security.VerificationException : Operation could destabilize the runtime.
exceptions. From all the unit tests I have 9 red ones with this exception. The exception is weird but not random, it comes every time when I run the tests. Did the unit tests run green for you?
Unfortunately the PR contains a lot of changes and it is hard to review, follow the ideas, and to see if this will work at the end. It is hard to see now:
- what change did break the tests (VerificationException)
- what changes are improving the performance significantly
- what are just nice-to-have formatting changes
The PR also changes the "public" API of ObjectContainer (sealed, removing protected virtual, etc.).
I'd ask/recommend to create smaller scoped PRs (starting with the most important improvement), keeping the changes focused and tested.
@SabotageAndi I think the single-file BoDi causes extra headache, because the diff of such bigger PRs is hardly readable. Probably we should split up the BoDi.cs file first before implementing such bigger changes in the lib.
@@ -225,92 +226,93 @@ public interface IStrategyRegistration | |||
IStrategyRegistration InstancePerContext(); | |||
} | |||
|
|||
public class ObjectContainer : IObjectContainer | |||
public sealed class ObjectContainer : IObjectContainer |
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.
Why do you want to seal the public ObjectContainer class? There might be library users who need to subclass.
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've generally seal all types, but I've come to learn that this is not generally wanted in SpecFlow, will adapt.
{ | ||
private readonly RegistrationKey currentRegistrationKey; | ||
private readonly Type currentResolvedType; | ||
private readonly ResolutionList nextNode; | ||
private bool IsLast { get { return nextNode == null; } } |
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.
What's the reason behind removing the IsLast and the two different ctors?
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.
- IsLast was only used in one place, where I felt it was simpler to just use nextNode != null. (not a relevant change)
- I only removed the non parameter ctor, as the usage changed to only create a ResolutionList when you need one (meaning you have at least one node)
if (type == null) throw new ArgumentNullException("type"); | ||
if (type is null) | ||
{ | ||
ThrowNullException(); |
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.
What's the reason behind this pattern? Why is the ThrowNullException local function better than the original if/throw?
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.
Generally: Throwing a exception prevents inlining of a function.
Since I moved the typeGroup into here, it might be inlined anyway (due to size of the ctor), so there might not be a benefit in doing so anymore.
{ | ||
return TypeGroup.GetHashCode(); | ||
} | ||
return typeGroup.GetHashCode(); |
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.
Is it safe to remove the unchecked here? Why was it here originally?
Note that in the original code not only the GetHasCode call was in unchecked but the whole TypeGroup calculation. Although in the Equals the TypeGroup was called without unchecked...
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 removed it as I didn't see a good reason why it was there, it gets a hashcode from a Type, so if unchecked is needed, shouldn't it be part of the Type.GetHashcode instead of here?
Also typeGroup is the cached field, TypeGroup was the property recalculating it all the time,.
var pooledObjectKey = new RegistrationKey(typeToConstruct, keyToResolve.Name); | ||
|
||
var result = ExecuteWithLock(syncRoot, () => container.GetPooledObject(pooledObjectKey), () => | ||
if (container.TryGetObjectFromPool(pooledObjectKey, out var obj)) |
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.
Does the copy-pasting of the ExecuteWithLock logic bring a significant performance improvement over the method call with lambdas? If yes, is there another way of optimizing this without copy-pasting the logic?
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 issue with these lambdas is that they contain local state, hence they have to be allocated for every execution, slowing it down due to a) allocation, b) GC overhead in cleaning it up, c) no inlining and d) call overhead.
So yes it is quite significant / measurable.
One possible alternative would be trying to extract certain pieces into their own functions that can be called individually, and thus trying to reduce the amount of code duplicated.
return registrations | ||
.Where(x => x.Key.Type == typeof(T)) | ||
.Select(x => Resolve(x.Key.Type, x.Key.Name) as T); | ||
foreach (var pair in this.registrations) |
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.
Is the foreach structure measurably faster than LINQ?
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.
Yes, measurably.
In general, LINQ are nice to read but horrible in performance (especially when they do closures).
Keep in mind we're here talking in ns difference, but as this is called so often during a normal test, they all add up.
OnObjectCreated(obj); | ||
|
||
return obj; | ||
} | ||
|
||
protected virtual void OnObjectCreated(object obj) | ||
private void OnObjectCreated(object obj) |
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.
Protected virtual to private is a breaking change for ObjectContainer (there might be external usages)
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.
does a derived implementation want to override this method or register themselves on ObjectCreated Event itself?
Also generic question: If someone actually wants to not use this ObjectContainer in their specflow scenario, how likely is it that one would implement their own vs derive from ObjectContainer 🤔
Also if someone would want to do it, wouldn't it be more likely to open a PR to fix something directly than implementing it in a derived class?
|
||
object obj; | ||
if (maxParamCountCtors.Length == 1) | ||
int maxLength = -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.
In this case the new solution looks much cleaner than the LINQ magic. ;)
@@ -909,22 +903,18 @@ private bool GetObjectFromPool(RegistrationKey pooledObjectKey, out object obj) | |||
|
|||
private object ResolveObject(RegistrationKey keyToResolve, ResolutionList resolutionPath) | |||
{ | |||
if (keyToResolve.Type.IsPrimitive || keyToResolve.Type == typeof(string) || keyToResolve.Type.IsValueType) | |||
throw new ObjectContainerException("Primitive types or structs cannot be resolved: " + keyToResolve.Type.FullName, resolutionPath.ToTypeList()); | |||
// All primitive types are structs, no need to check |
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.
Good point.
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.
Also FYI, IsPrimitive is not an intrinsic, hence it is generally not as cheap to call as IsValueType.
private readonly ObjectContainer baseContainer; | ||
private readonly ConcurrentDictionary<RegistrationKey, IRegistration> registrations = new ConcurrentDictionary<RegistrationKey, IRegistration>(); | ||
private readonly List<RegistrationKey> resolvedKeys = new List<RegistrationKey>(); | ||
private readonly HashSet<RegistrationKey> resolvedKeys = new HashSet<RegistrationKey>(); |
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 have this "too nice to be true" feeling about this: it is "too obvious" that a HashSet would be more performant instead of the List (assuming a big enough list). I wonder if there was a good (but unfortunately undocumented) reason for using a List here in the original code.
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 wasn't able to find one, if there really is one, happy to check again what optimization we could apply then.
I have now an environment that I can reproduce it, so I might be able to find the root cause for it. |
I've played around a bit and saw a lot of potential for improvements. The result should (😉 ) not alter public behavior but rather only improve performance / allocations:
This is the outcome for one of the Benchmarks
Master
PR
PS: I got some weird exception when I tried to run it on .net 4.5
System.Security.VerificationException : Operation could destabilize the runtime.
so could you please check whether this is a "doesn't work on my machine"-issue :)