Optimizations for ObjectExtensions#759
Conversation
| var paramType1 = typeof(TArg1); | ||
|
|
||
| object cachedItem = Cache.GetOrAdd( | ||
| $"{type.AssemblyQualifiedName}.{methodName}.{paramType1.AssemblyQualifiedName}", |
There was a problem hiding this comment.
I'm assuming that the comparer for System.Type has the same granularity as the assembly qualified name. It would make sense, but I need to double-check
There was a problem hiding this comment.
The AssemblyQualified type was also useful because we want the owning Assembly as well. This is something that we can run into with the application using both StackExchange.Redis.StrongName and StackExchange.Redis NuGet packages
There was a problem hiding this comment.
I assumed as much yes. That's why I need to make absolutely sure that the Type equality comparer takes that point into account.
There was a problem hiding this comment.
Looking good. I created a library and compiled it twice: once in debug without signature, once in release with a strong name (the debug/release distinction is probably irrelevant)
Then ran the following code:
var debug = Assembly.LoadFrom(@"C:\Users\kevin.gosse\source\repos\ConsoleApp1\Library1\bin\Debug\Library1.dll");
var release = Assembly.LoadFrom(@"C:\Users\kevin.gosse\source\repos\ConsoleApp1\Library1\bin\Release\Library1.dll");
var typeDebug = debug.GetType("Library1.Class1");
var typeRelease = release.GetType("Library1.Class1");
Console.WriteLine(debug.FullName);
Console.WriteLine(release.FullName);
Console.WriteLine($"{typeDebug.FullName} - {typeDebug.AssemblyQualifiedName}");
Console.WriteLine($"{typeRelease.FullName} - {typeRelease.AssemblyQualifiedName}");
Console.WriteLine(typeDebug.Equals(typeRelease));The output is:
Library1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
Library1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=c28eafe45d72d6c9
Library1.Class1 - Library1.Class1, Library1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
Library1.Class1 - Library1.Class1, Library1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=c28eafe45d72d6c9
False
So it looks like the type equality comparer does the necessary checks.
| return dynamicMethod.CreateDelegate(); | ||
| } | ||
|
|
||
| private readonly struct PropertyFetcherCacheKey : IEquatable<PropertyFetcherCacheKey> |
There was a problem hiding this comment.
I'm using the same key type for methods and properties. As far as I know, a type can't have a property and a method with the same name, so I don't believe collisions are possible
| Name = name; | ||
| } | ||
|
|
||
| public bool Equals(PropertyFetcherCacheKey other) |
There was a problem hiding this comment.
Equality members auto-generated by resharper
| /// <returns>The value of the property on the specified object.</returns> | ||
| public T Fetch<T>(object obj, Type objType) | ||
| { | ||
| if (objType != _expectedType) |
There was a problem hiding this comment.
I don't really understand why we do that. If we call Fetch with a different type, shouldn't we throw?
If it's done just for initialization, shouldn't we do that in the constructor?
There was a problem hiding this comment.
I'm not entirely sure why we did it this way where we reconstruct the fetcher if the type of the object is different (other than the fact that we ported this directly from dotnet/runtime). It seems like the only upside is that this allows us to resolve properties against multiple implementation types of the same abstract type, but I think we could accomplish this by getting the type of the generic parameter T and it would work for all method arguments.
There was a problem hiding this comment.
@lucaspimentel do you have any more insight on this?
There was a problem hiding this comment.
This file was copied verbatim from the dotnet repo (see url at top of file). Unfortunately, that link is to master and the file has since changed. We also made this one change which I think was to allow getting inherited properties (not declared in the given type).
My guess is that DiagnosticSource needed to handle changing types because they rely heavily on anonymous types. For example, a fetcher for a property called Name could be called once with
new { Name = "Foo" }
and then later with
new { Name = "Foo", Value = 1 }
... and each time the type changed, they generate a new delegate. I don't think we need to support this "feature" for our use cases.
There was a problem hiding this comment.
For reference, I think the version we used was from .NET Core 2.1: source
| private readonly string _propertyName; | ||
| private Type _expectedType; | ||
| private PropertyFetch _fetchForExpectedType; | ||
| private object _fetchForExpectedType; |
There was a problem hiding this comment.
It bothers me a bit. I'll probably come back to it at some point to try and remove the cast
b86e439 to
ab76ce3
Compare
| object cachedItem = Cache.GetOrAdd( | ||
| GetKey<TResult>(fieldName, type), | ||
| key => CreateFieldDelegate<TResult>(type, fieldName)); | ||
| key => CreateFieldDelegate<TResult>(key.Type1, key.Name)); |
|
Pushed a new version that uses expressions to fetch the property value. I thought I was being clever with the With the latest commit:
New benchmark: BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.329 (2004/?/20H1)
Intel Core i7-9750H CPU 2.60GHz, 1 CPU, 12 logical and 6 physical cores
[Host] : .NET Framework 4.8 (4.8.4180.0), X64 RyuJIT
DefaultJob : .NET Framework 4.8 (4.8.4180.0), X64 RyuJIT
NewGetProperty => Previous version of the code (that didn't work with the enum -> int conversion) |
- Make MemberResult a struct - Add a custom key type for the cache to avoid building a string every time - Use the custom key to prevent closure allocation in the Concurrent.GetOrAdd callbacks - Make the PropertyFetcher generic to prevent boxing when fetching a value type - Add a Fetch overload to provide the type when it's already known
- Messed up the orders of the parameters when creating the key for fields - Didn't consider that somebody could ask for a value of a type different from the property (for instance, GetProperty<object> on an int property)
Enforce that Type1 and Name are not null, and remove the null-check from GetHashCode
cb0d8c1 to
4f8dbe6
Compare
zacharycmontoya
left a comment
There was a problem hiding this comment.
LGTM in its current state. I'm not sure if you had planned to make any additional changes
Benchmark: