Skip to content

RuntimePropertyInfo performance regression when used in a hash table #114280

Open
@jairbubbles

Description

@jairbubbles

Description

When migrating some code from net472 to net8.0 I noticed a huge performance regression with some code dealing with caching MemberInfo attributes in a ConcurrentDictionary.

I investigated the issue and narrowed it down to a simple sample:

HashSet<PropertyInfo> propertyInfos = new();
foreach (PropertyInfo property in GetManyProperties())
{
     propertyInfos.Add(property);
}

In my benchmark I'm testing with the Graph API assembly. It has a lot of types (120k properties in total!), which is perfect to make the issue more visible, in this condition I get:

| Method                      | Job                  | Runtime              | Mean       | Error      | StdDev     | Ratio | RatioSD |
|---------------------------- |--------------------- |--------------------- |-----------:|-----------:|-----------:|------:|--------:|
| AddToHashSet_WithBaseHash   | .NET 8.0             | .NET 8.0             | 637.151 ms | 12.3232 ms | 15.5849 ms |  1.00 |    0.03 |
| AddToHashSet_WithBaseHash   | .NET Framework 4.7.2 | .NET Framework 4.7.2 |   7.236 ms |  0.1813 ms |  0.5287 ms |  1.01 |    0.10 |

When profiling I noticed that we were spending a lot of time in Equals as many elements were put in the same bucket. Those elements were all the properties which are shared by inheritance. They shared the same hash code but Equals would always return false.

I believe the issue was introduced by #78249 where ReflectedType was added as a criteria for equality but not the hash code computation.

Workaround

As a workaround I'm using a custom EqualityComparer which also uses the actual type (not only the declaring type)

public int GetHashCode(PropertyInfo obj)
{
  // Take in account the ReflectedType as it's also used in RuntimePropertyInfo.Equals
  return HashCode.Combine(obj.MetadataToken, obj.DeclaringType!.GetHashCode(), obj.ReflectedType!.GetHashCode());
}

In those conditions .NET 8 becomes faster:

Method Job Runtime Mean Error StdDev Ratio RatioSD
AddToHashSet_WithCustomHash .NET 8.0 .NET 8.0 9.529 ms 0.1877 ms 0.3876 ms 0.01 0.00
AddToHashSet_WithCustomHash .NET Framework 4.7.2 .NET Framework 4.7.2 12.815 ms 0.2563 ms 0.4281 ms 1.78 0.14

Sample

Here is the full benchmark project: Benchmark.zip

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions