Skip to content

Conversation

@dennisdoomen
Copy link
Member

Tries to compensate for the performance regression in #1708

@jnyrup
Copy link
Member

jnyrup commented Oct 22, 2021

Gave it a spin.

Runtime N Depth Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
.NET 5.0 1 1 46.52 us 0.631 us 0.560 us 8.7280 0.1831 - 54 KB
.NET Framework 4.7.2 1 1 64.35 us 0.573 us 0.536 us 9.3994 0.1221 - 58 KB
.NET 5.0 1 2 58.26 us 0.485 us 0.454 us 11.4746 0.3052 - 71 KB
.NET Framework 4.7.2 1 2 82.32 us 0.718 us 0.636 us 12.4512 0.2441 - 77 KB
.NET 5.0 1 6 109.27 us 1.273 us 1.190 us 22.7051 0.7324 - 139 KB
.NET Framework 4.7.2 1 6 149.39 us 1.231 us 1.152 us 24.9023 0.7324 - 154 KB
.NET 5.0 10 1 293.40 us 1.405 us 1.173 us 61.5234 1.9531 - 378 KB
.NET Framework 4.7.2 10 1 400.31 us 2.347 us 2.196 us 67.8711 2.4414 - 419 KB
.NET 5.0 10 2 417.96 us 6.401 us 5.674 us 88.8672 3.4180 - 545 KB
.NET Framework 4.7.2 10 2 564.46 us 4.759 us 4.452 us 98.6328 3.9063 - 607 KB
.NET 5.0 10 6 918.89 us 10.393 us 9.213 us 200.1953 11.7188 - 1,231 KB
.NET Framework 4.7.2 10 6 1,247.44 us 12.598 us 11.784 us 222.6563 11.7188 - 1,373 KB
.NET 5.0 100 1 2,831.94 us 48.238 us 45.122 us 593.7500 70.3125 - 3,642 KB
.NET Framework 4.7.2 100 1 3,752.11 us 36.804 us 34.427 us 656.2500 70.3125 - 4,040 KB
.NET 5.0 100 2 4,073.50 us 78.885 us 69.930 us 867.1875 125.0000 - 5,334 KB
.NET Framework 4.7.2 100 2 5,499.83 us 48.891 us 45.732 us 960.9375 125.0000 - 5,929 KB
.NET 5.0 100 6 9,040.22 us 93.941 us 83.276 us 1984.3750 312.5000 - 12,229 KB
.NET Framework 4.7.2 100 6 12,340.11 us 89.188 us 83.427 us 2203.1250 62.5000 - 13,628 KB
.NET 5.0 500 1 13,541.19 us 113.585 us 106.248 us 2953.1250 93.7500 - 18,141 KB
.NET Framework 4.7.2 500 1 18,651.13 us 204.715 us 191.491 us 3250.0000 125.0000 - 20,124 KB
.NET 5.0 500 2 21,067.54 us 289.728 us 271.012 us 4312.5000 93.7500 31.2500 26,605 KB
.NET Framework 4.7.2 500 2 26,739.77 us 54.964 us 48.725 us 4812.5000 156.2500 31.2500 29,574 KB
.NET 5.0 500 6 47,192.44 us 768.044 us 680.851 us 9909.0909 454.5455 - 61,089 KB
.NET Framework 4.7.2 500 6 62,535.11 us 393.748 us 368.312 us 11000.0000 625.0000 250.0000 68,078 KB

Relative and absolute differences compared to before #1708

Runtime N Depth time rel time abs GC rel GC abs
.NET 5.0 1 1 1.05 2.08 us 1.00 0 KB
.NET Framework 4.7.2 1 1 1.08 5.02 us 0.98 -1 KB
.NET 5.0 1 2 0.98 -1.03 us 0.97 -2 KB
.NET Framework 4.7.2 1 2 1.08 6.03 us 0.97 -2 KB
.NET 5.0 1 6 0.93 -7.85 us 0.93 -10 KB
.NET Framework 4.7.2 1 6 1.02 2.45 us 0.94 -10 KB
.NET 5.0 10 1 0.94 -17.92 us 0.92 -32 KB
.NET Framework 4.7.2 10 1 1.03 10.13 us 0.93 -32 KB
.NET 5.0 10 2 0.94 -25.43 us 0.92 -50 KB
.NET Framework 4.7.2 10 2 1.02 8.74 us 0.92 -50 KB
.NET 5.0 10 6 0.92 -77.26 us 0.91 -122 KB
.NET Framework 4.7.2 10 6 0.95 -62.04 us 0.92 -123 KB
.NET 5.0 100 1 0.93 -219.34 us 0.91 -349 KB
.NET Framework 4.7.2 100 1 0.99 -55.15 us 0.92 -350 KB
.NET 5.0 100 2 0.90 -441.35 us 0.91 -528 KB
.NET Framework 4.7.2 100 2 0.98 -134.91 us 0.92 -532 KB
.NET 5.0 100 6 0.87 -1343.71 us 0.91 -1256 KB
.NET Framework 4.7.2 100 6 0.97 -432.85 us 0.92 -1265 KB
.NET 5.0 500 1 0.88 -1870.93 us 0.91 -1756 KB
.NET Framework 4.7.2 500 1 0.96 -816.05 us 0.92 -1761 KB
.NET 5.0 500 2 0.91 -1960.57 us 0.91 -2655 KB
.NET Framework 4.7.2 500 2 0.93 -2161.54 us 0.92 -2670 KB
.NET 5.0 500 6 0.89 -5786.24 us 0.91 -6298 KB
.NET Framework 4.7.2 500 6 0.97 -1826.73 us 0.91 -6338 KB

@dennisdoomen
Copy link
Member Author

Thanks. I still have to do that myself when I'm back on my dev machine. The way I read this is that on .NET 4.7, the dictionary adds a little bit of overhead, but with more tests and more depth, it becomes faster. But I was hoping to get a larger improvement here.

How did you build that comparison chart?

@jnyrup
Copy link
Member

jnyrup commented Oct 23, 2021

A potential bias with this benchmark is that it favors adding caches of types, as it reuses ComplexType over and over.

I manually created the comparison chart using a spreadsheet.

@dennisdoomen dennisdoomen force-pushed the Feature/ReflectionPerformance branch from 3fafe22 to 26dbbaf Compare October 23, 2021 13:37
@dennisdoomen
Copy link
Member Author

To my surprise, first checking the size of the referenceTypes and valueTypes collections gave us another 5% improvement. But then I started a dotTrace session on the benchmarks process and discovered that listing the properties and fields of a type were taking a big chunk of the time, so I added some more caching giving us another 30% improvement. I'm assuming that the number of types cached is never going to become a memory issue.

@dennisdoomen dennisdoomen marked this pull request as ready for review October 23, 2021 13:47
@dennisdoomen dennisdoomen force-pushed the Feature/ReflectionPerformance branch from 26dbbaf to b5ea2a4 Compare October 23, 2021 13:49
@dennisdoomen dennisdoomen requested a review from jnyrup October 23, 2021 14:07
Copy link
Member

@jnyrup jnyrup left a comment

Choose a reason for hiding this comment

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

Concern

Since we're about to cache EqualityStrategy.
Are we sure that one cannot in any (reasonable) way modify any of referenceTypes, valueTypes, compareRecordsByValue or getDefaultEqualityStrategy?

E.g. this code compiles and while strategy2 should be different from strategy we have already cached an equality strategy for IEnumerable.

var options = new EquivalencyAssertionOptions();
EqualityStrategy strategy = options.As<IEquivalencyAssertionOptions>().GetEqualityStrategy(typeof(IEnumerable));
options.ComparingByMembers<IEnumerable>();
EqualityStrategy strategy2 = options.As<IEquivalencyAssertionOptions>().GetEqualityStrategy(typeof(IEnumerable));

This was discovered by adding static to the lambdas for GetOrdAdd to see if they captured any state.

Benchmarks

Runtime N Depth Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
.NET 5.0 1 1 37.05 us 0.306 us 0.271 us 7.2021 0.1831 - 44 KB
.NET Framework 4.7.2 1 1 53.30 us 0.726 us 0.643 us 7.5073 0.1831 - 46 KB
.NET 5.0 1 2 46.12 us 0.355 us 0.315 us 9.3384 0.2441 - 58 KB
.NET Framework 4.7.2 1 2 65.97 us 0.619 us 0.549 us 9.7656 0.2441 - 60 KB
.NET 5.0 1 6 85.83 us 0.568 us 0.531 us 18.1885 0.6104 - 112 KB
.NET Framework 4.7.2 1 6 112.31 us 1.475 us 1.307 us 19.0430 0.6104 - 117 KB
.NET 5.0 10 1 224.41 us 1.134 us 1.061 us 49.3164 1.7090 - 303 KB
.NET Framework 4.7.2 10 1 296.11 us 3.825 us 3.578 us 51.7578 1.4648 - 318 KB
.NET 5.0 10 2 321.93 us 2.029 us 1.898 us 70.8008 2.9297 - 434 KB
.NET Framework 4.7.2 10 2 415.42 us 4.357 us 3.862 us 74.2188 2.9297 - 457 KB
.NET 5.0 10 6 731.81 us 4.058 us 3.796 us 158.2031 9.7656 - 974 KB
.NET Framework 4.7.2 10 6 968.98 us 19.304 us 32.253 us 166.0156 8.7891 - 1,025 KB
.NET 5.0 100 1 2,165.82 us 11.930 us 11.159 us 472.6563 54.6875 - 2,911 KB
.NET Framework 4.7.2 100 1 2,713.54 us 33.405 us 31.247 us 496.0938 50.7813 - 3,050 KB
.NET 5.0 100 2 3,120.49 us 10.969 us 9.723 us 691.4063 97.6563 - 4,239 KB
.NET Framework 4.7.2 100 2 3,961.89 us 40.942 us 31.965 us 718.7500 93.7500 - 4,446 KB
.NET 5.0 100 6 6,904.14 us 56.297 us 52.660 us 1578.1250 289.0625 - 9,677 KB
.NET Framework 4.7.2 100 6 8,946.78 us 98.731 us 87.523 us 1640.6250 281.2500 15.6250 10,170 KB
.NET 5.0 500 1 10,518.87 us 43.500 us 40.690 us 2359.3750 46.8750 - 14,497 KB
.NET Framework 4.7.2 500 1 14,044.45 us 122.977 us 115.033 us 2468.7500 31.2500 - 15,184 KB
.NET 5.0 500 2 15,943.30 us 107.601 us 100.650 us 3437.5000 62.5000 - 21,140 KB
.NET Framework 4.7.2 500 2 19,830.24 us 128.631 us 120.322 us 3593.7500 62.5000 - 22,165 KB
.NET 5.0 500 6 36,714.90 us 211.049 us 197.415 us 7857.1429 357.1429 - 48,341 KB
.NET Framework 4.7.2 500 6 44,841.02 us 151.675 us 134.456 us 8250.0000 333.3333 83.3333 50,793 KB
Runtime N Depth Time rel Time abs GC rel GC abs
.NET 5.0 1 1 0.83 -7.39 us 0.81 -10 KB
.NET Framework 4.7.2 1 1 0.90 -6.03 us 0.78 -13 KB
.NET 5.0 1 2 0.78 -13.17 us 0.79 -15 KB
.NET Framework 4.7.2 1 2 0.86 -10.32 us 0.76 -19 KB
.NET 5.0 1 6 0.73 -31.29 us 0.75 -37 KB
.NET Framework 4.7.2 1 6 0.76 -34.63 us 0.71 -47 KB
.NET 5.0 10 1 0.72 -86.91 us 0.74 -107 KB
.NET Framework 4.7.2 10 1 0.76 -94.07 us 0.71 -133 KB
.NET 5.0 10 2 0.73 -121.46 us 0.73 -161 KB
.NET Framework 4.7.2 10 2 0.75 -140.30 us 0.70 -200 KB
.NET 5.0 10 6 0.73 -264.34 us 0.72 -379 KB
.NET Framework 4.7.2 10 6 0.74 -340.50 us 0.69 -471 KB
.NET 5.0 100 1 0.71 -885.46 us 0.73 -1080 KB
.NET Framework 4.7.2 100 1 0.71 -1093.72 us 0.69 -1340 KB
.NET 5.0 100 2 0.69 -1394.36 us 0.72 -1623 KB
.NET Framework 4.7.2 100 2 0.70 -1672.85 us 0.69 -2015 KB
.NET 5.0 100 6 0.66 -3479.79 us 0.72 -3808 KB
.NET Framework 4.7.2 100 6 0.70 -3826.18 us 0.68 -4723 KB
.NET 5.0 500 1 0.68 -4893.25 us 0.73 -5400 KB
.NET Framework 4.7.2 500 1 0.72 -5422.73 us 0.69 -6701 KB
.NET 5.0 500 2 0.69 -7084.81 us 0.72 -8120 KB
.NET Framework 4.7.2 500 2 0.69 -9071.07 us 0.69 -10079 KB
.NET 5.0 500 6 0.69 -16263.78 us 0.72 -19046 KB
.NET Framework 4.7.2 500 6 0.70 -19520.82 us 0.68 -23623 KB

@dennisdoomen
Copy link
Member Author

Are we sure that one cannot in any (reasonable) way modify any of referenceTypes, valueTypes, compareRecordsByValue or getDefaultEqualityStrategy?

I've thought about that as well, but given how the options are not reconfigured after the actual object graph comparison starts, it should be save.

@dennisdoomen dennisdoomen force-pushed the Feature/ReflectionPerformance branch from b5ea2a4 to 2eb75ff Compare October 23, 2021 17:17
@jnyrup
Copy link
Member

jnyrup commented Oct 23, 2021

Here's a valid example that breaks with caching of EqualityStrategy.

public class Class1
{
    [Fact]
    public void RunMeFirst()
    {
        new MyClass { Value = 1 }.Should().BeEquivalentTo(new MyClass { Value = 1 });
    }
}

// Non-parallel tests are typically run after all parallel tests
[Collection("AssertionOptionsSpecs")]
public sealed class Class2 : IDisposable
{
    public Class2()
    {
        AssertionOptions.AssertEquivalencyUsing(opt => opt.ComparingByValue<MyClass>());
    }

    [Fact]
    public void RunMeSecond()
    {
        new MyClass { Value = 1 }.Should().NotBeEquivalentTo(new MyClass { Value = 1 });
    }

    public void Dispose()
    {
        AssertionOptions.AssertEquivalencyUsing(_ => new EquivalencyAssertionOptions());
    }
}

public class MyClass
{
    public int Value { get; set; }
}

[CollectionDefinition("AssertionOptionsSpecs", DisableParallelization = true)]
public class AssertionOptionsSpecsDefinition { }

@jnyrup
Copy link
Member

jnyrup commented Oct 23, 2021

And the numbers for leaving out caching EqualityStrategy.

Runtime N Depth Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
.NET 5.0 1 1 39.30 us 0.315 us 0.295 us 6.7749 0.1221 - 42 KB
.NET Framework 4.7.2 1 1 52.10 us 0.920 us 0.861 us 7.1411 0.1221 - 44 KB
.NET 5.0 1 2 49.47 us 0.329 us 0.308 us 8.9722 0.1831 - 55 KB
.NET Framework 4.7.2 1 2 64.80 us 0.860 us 0.805 us 9.3994 0.1221 - 58 KB
.NET 5.0 1 6 92.53 us 0.246 us 0.230 us 17.8223 0.4883 - 109 KB
.NET Framework 4.7.2 1 6 118.86 us 2.225 us 1.858 us 18.6768 0.4883 - 115 KB
.NET 5.0 10 1 247.23 us 1.687 us 1.578 us 49.0723 1.4648 - 301 KB
.NET Framework 4.7.2 10 1 304.40 us 6.042 us 6.205 us 51.2695 1.4648 - 316 KB
.NET 5.0 10 2 350.12 us 1.886 us 1.672 us 70.3125 2.4414 - 432 KB
.NET Framework 4.7.2 10 2 428.14 us 4.866 us 4.313 us 73.7305 2.4414 - 455 KB
.NET 5.0 10 6 815.31 us 7.427 us 6.947 us 158.2031 8.7891 - 973 KB
.NET Framework 4.7.2 10 6 958.36 us 13.303 us 11.793 us 166.0156 7.8125 - 1,025 KB
.NET 5.0 100 1 2,365.49 us 32.227 us 30.145 us 472.6563 50.7813 - 2,915 KB
.NET Framework 4.7.2 100 1 2,877.57 us 33.110 us 30.972 us 496.0938 50.7813 - 3,054 KB
.NET 5.0 100 2 3,479.72 us 67.779 us 63.400 us 691.4063 89.8438 - 4,246 KB
.NET Framework 4.7.2 100 2 4,111.55 us 16.032 us 14.996 us 718.7500 85.9375 - 4,453 KB
.NET 5.0 100 6 7,754.52 us 101.397 us 94.847 us 1578.1250 289.0625 - 9,697 KB
.NET Framework 4.7.2 100 6 9,458.43 us 18.867 us 15.755 us 1656.2500 281.2500 15.6250 10,190 KB
.NET 5.0 500 1 11,701.30 us 140.508 us 131.431 us 2359.3750 46.8750 - 14,526 KB
.NET Framework 4.7.2 500 1 14,295.51 us 25.923 us 24.248 us 2468.7500 31.2500 - 15,213 KB
.NET 5.0 500 2 17,451.66 us 221.820 us 196.638 us 3437.5000 62.5000 - 21,185 KB
.NET Framework 4.7.2 500 2 20,710.31 us 40.436 us 35.845 us 3593.7500 62.5000 - 22,210 KB
.NET 5.0 500 6 39,241.05 us 444.962 us 416.217 us 7846.1538 384.6154 - 48,447 KB
.NET Framework 4.7.2 500 6 48,900.80 us 394.394 us 329.337 us 8272.7273 545.4545 181.8182 50,901 KB

@dennisdoomen
Copy link
Member Author

Here's a valid example that breaks with caching of EqualityStrategy.

Did you test this? I haven't tried myself yet, but I'm wondering how that could fail. The cache is per instance of the SelfReferenceEquivalencyAssertionOptions

@jnyrup
Copy link
Member

jnyrup commented Oct 23, 2021

I did and and just double checked.
Here's the flow of what's happening

class1.RunMeFirst
    GetEqualityStrategy(MyClass) -> cache miss
        getDefaultEqualityStrategy(type) -> cache miss
            strategy = type.HasValueSemantics() ? EqualityStrategy.Equals : EqualityStrategy.Members;
                EqualityStrategy.Members
    GetEqualityStrategy(MyClass) -> cache hit
    
Class2.ctor
    AssertionOptions.AssertEquivalencyUsing(opt => opt.ComparingByValue<MyClass>());

class2.RunMeSecond
    GetEqualityStrategy(MyClass) -> cache miss
        getDefaultEqualityStrategy(type) -> cache hit // here's the bug, as valueTypes has been changed since equalityStrategyCache was populated
            EqualityStrategy.Members
    GetEqualityStrategy(MyClass) -> cache hit

class2.Dispose
    AssertionOptions.AssertEquivalencyUsing(_ => new EquivalencyAssertionOptions());

@dennisdoomen dennisdoomen force-pushed the Feature/ReflectionPerformance branch from 2eb75ff to ebcf4ce Compare October 24, 2021 15:17
@dennisdoomen
Copy link
Member Author

I don't even get how you figured that out.

Anyway, I think I've fixed it now.

@dennisdoomen dennisdoomen requested a review from jnyrup October 24, 2021 15:17
@dennisdoomen dennisdoomen force-pushed the Feature/ReflectionPerformance branch from ebcf4ce to 953d297 Compare October 24, 2021 18:14
@dennisdoomen dennisdoomen merged commit a529e8f into fluentassertions:master Oct 24, 2021
@dennisdoomen dennisdoomen deleted the Feature/ReflectionPerformance branch October 24, 2021 19:53
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.

2 participants