Skip to content

Conversation

@dennisdoomen
Copy link
Member

@dennisdoomen dennisdoomen commented Oct 16, 2021

When an object graph implementing value semantics (by overriding Equals) contains cyclic references, the IgnoringCyclicReferences option did not work.

Fixes #1370

@dennisdoomen dennisdoomen changed the title IgnoreCyclicDependencies in BeEquivalentTo now works with ComparingByMembers IgnoreCyclicReferences in BeEquivalentTo now works with ComparingByMembers Oct 16, 2021
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.

While correctness is more important that speed, I'm curious what effect deletion of the cache has.

@dennisdoomen
Copy link
Member Author

While correctness is more important that speed, I'm curious what effect deletion of the cache has.

I thought about that, but the GetEqualityStrategy method uses its own cache. The only concern I had is that the IsRecord method might be expensive as well, but looking at the build time, it doesn't seem to have such an effect. I'll run a profile session on my big machine when I have time.

@dennisdoomen dennisdoomen merged commit b6bab8f into fluentassertions:master Oct 17, 2021
@dennisdoomen dennisdoomen deleted the Fix/1370 branch October 17, 2021 06:38
@jnyrup
Copy link
Member

jnyrup commented Oct 20, 2021

I added the caching of OverridesEquals in 12bd297.
I've tried to rerun the benchmark code from #817 before and after this PR to get a sense of the impact.
Note, I haven't given much thought into, if this the most representative benchmark.

using System.Collections.Generic;
using System.Linq;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using FluentAssertions;

namespace Benchmarks
{
    [MemoryDiagnoser]
    [SimpleJob(RuntimeMoniker.Net472)]
    [SimpleJob(RuntimeMoniker.Net50)]
    public class PR817
    {
        public class Complex
        {
            public int A { get; set; }
            public Complex B { get; set; }
        }

        private static Complex CreateComplex(int i)
        {
            if (i == 0)
            {
                return new Complex();
            }

            return new Complex
            {
                A = i,
                B = CreateComplex(i - 1)
            };
        }

        [Params(1, 10, 100, 500)]
        public int N { get; set; }

        [Params(1, 2, 6)]
        public int Depth { get; set; }

        [GlobalSetup]
        public void GlobalSetup()
        {
            list = Enumerable.Range(0, N).Select(_ => CreateComplex(Depth)).ToList();
            list2 = Enumerable.Range(0, N).Select(_ => CreateComplex(Depth)).ToList();
        }

        private List<Complex> list;
        private List<Complex> list2;

        [Benchmark]
        public void BeEquivalentTo()
        {
            list.Should().BeEquivalentTo(list2);
        }
    }
}

Before - 9190b26

Runtime N Depth Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
.NET 5.0 1 1 44.44 us 0.549 us 0.459 us 8.7891 0.2441 - 54 KB
.NET Framework 4.7.2 1 1 59.33 us 0.893 us 0.835 us 9.5215 0.1831 - 59 KB
.NET 5.0 1 2 59.29 us 0.850 us 0.795 us 11.8408 0.3052 - 73 KB
.NET Framework 4.7.2 1 2 76.29 us 0.418 us 0.391 us 12.8174 0.2441 - 79 KB
.NET 5.0 1 6 117.12 us 1.630 us 1.525 us 24.1699 0.8545 - 149 KB
.NET Framework 4.7.2 1 6 146.94 us 0.628 us 0.524 us 26.6113 0.7324 - 164 KB
.NET 5.0 10 1 311.32 us 2.154 us 1.909 us 66.4063 2.4414 - 410 KB
.NET Framework 4.7.2 10 1 390.18 us 5.196 us 4.860 us 73.2422 2.4414 - 451 KB
.NET 5.0 10 2 443.39 us 3.349 us 3.133 us 97.1680 3.9063 - 595 KB
.NET Framework 4.7.2 10 2 555.72 us 5.050 us 4.477 us 106.4453 3.9063 - 657 KB
.NET 5.0 10 6 996.15 us 8.023 us 7.504 us 220.7031 13.6719 - 1,353 KB
.NET Framework 4.7.2 10 6 1,309.48 us 24.767 us 27.528 us 242.1875 13.6719 - 1,496 KB
.NET 5.0 100 1 3,051.28 us 22.837 us 21.362 us 648.4375 74.2188 - 3,991 KB
.NET Framework 4.7.2 100 1 3,807.26 us 59.104 us 52.394 us 710.9375 70.3125 - 4,390 KB
.NET 5.0 100 2 4,514.85 us 90.044 us 84.227 us 953.1250 125.0000 - 5,862 KB
.NET Framework 4.7.2 100 2 5,634.74 us 81.959 us 76.664 us 1046.8750 125.0000 - 6,461 KB
.NET 5.0 100 6 10,383.93 us 199.423 us 221.658 us 2187.5000 62.5000 - 13,485 KB
.NET Framework 4.7.2 100 6 12,772.96 us 189.072 us 176.858 us 2421.8750 46.8750 - 14,893 KB
.NET 5.0 500 1 15,412.12 us 209.976 us 186.138 us 3234.3750 46.8750 - 19,897 KB
.NET Framework 4.7.2 500 1 19,467.18 us 276.629 us 258.759 us 3531.2500 125.0000 - 21,885 KB
.NET 5.0 500 2 23,028.11 us 286.750 us 254.196 us 4750.0000 93.7500 31.2500 29,260 KB
.NET Framework 4.7.2 500 2 28,901.31 us 486.069 us 454.669 us 5218.7500 62.5000 - 32,244 KB
.NET 5.0 500 6 52,978.68 us 789.637 us 699.993 us 11000.0000 400.0000 - 67,387 KB
.NET Framework 4.7.2 500 6 64,361.84 us 900.284 us 798.078 us 12000.0000 1125.0000 250.0000 74,416 KB

After - b6bab8f

Runtime N Depth Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
.NET 5.0 1 1 47.13 us 0.444 us 0.415 us 9.7046 0.1831 - 59 KB
.NET Framework 4.7.2 1 1 63.20 us 1.000 us 0.935 us 10.3760 0.2441 - 64 KB
.NET 5.0 1 2 62.11 us 0.530 us 0.496 us 13.0615 0.3662 - 80 KB
.NET Framework 4.7.2 1 2 82.92 us 0.905 us 0.847 us 14.0381 0.3662 - 87 KB
.NET 5.0 1 6 124.88 us 1.555 us 1.455 us 26.6113 0.7324 - 164 KB
.NET Framework 4.7.2 1 6 156.08 us 1.845 us 1.726 us 29.0527 0.9766 - 179 KB
.NET 5.0 10 1 333.27 us 1.981 us 1.756 us 73.2422 2.4414 - 450 KB
.NET Framework 4.7.2 10 1 423.18 us 4.823 us 4.511 us 79.5898 2.4414 - 492 KB
.NET 5.0 10 2 477.20 us 2.460 us 2.301 us 106.9336 4.3945 - 656 KB
.NET Framework 4.7.2 10 2 595.42 us 6.278 us 5.565 us 116.2109 3.9063 - 718 KB
.NET 5.0 10 6 1,069.91 us 4.015 us 3.559 us 242.1875 13.6719 - 1,493 KB
.NET Framework 4.7.2 10 6 1,329.76 us 17.627 us 16.488 us 265.6250 13.6719 - 1,636 KB
.NET 5.0 100 1 3,251.89 us 47.996 us 44.896 us 714.8438 85.9375 - 4,386 KB
.NET Framework 4.7.2 100 1 4,035.93 us 50.549 us 44.811 us 773.4375 78.1250 - 4,786 KB
.NET 5.0 100 2 4,774.59 us 23.724 us 22.192 us 1046.8750 132.8125 - 6,454 KB
.NET Framework 4.7.2 100 2 5,965.34 us 104.865 us 98.091 us 1140.6250 132.8125 - 7,055 KB
.NET 5.0 100 6 11,101.92 us 116.930 us 103.656 us 2421.8750 140.6250 - 14,865 KB
.NET Framework 4.7.2 100 6 13,770.33 us 91.639 us 85.719 us 2640.6250 312.5000 - 16,277 KB
.NET 5.0 500 1 16,846.90 us 295.231 us 276.159 us 3562.5000 62.5000 - 21,868 KB
.NET Framework 4.7.2 500 1 20,292.77 us 292.774 us 273.861 us 3875.0000 93.7500 - 23,861 KB
.NET 5.0 500 2 24,206.03 us 337.620 us 299.291 us 5250.0000 93.7500 31.2500 32,215 KB
.NET Framework 4.7.2 500 2 30,751.46 us 113.126 us 105.818 us 5718.7500 62.5000 - 35,208 KB
.NET 5.0 500 6 56,910.38 us 475.757 us 421.747 us 12111.1111 444.4444 - 74,279 KB
.NET Framework 4.7.2 500 6 68,723.85 us 1,301.764 us 1,217.671 us 13125.0000 500.0000 125.0000 81,330 KB

Relative + absolute diffs

Job N Depth Rel Time Abs Time Rel Allocations Abs Allocations
.NET 5.0 1 1 1.061 2.69 us 1.093 5 KB
.NET Framework 4.7.2 1 1 1.065 3.87 us 1.085 5 KB
.NET 5.0 1 2 1.048 2.82 us 1.096 7 KB
.NET Framework 4.7.2 1 2 1.087 6.63 us 1.101 8 KB
.NET 5.0 1 6 1.066 7.76 us 1.101 15 KB
.NET Framework 4.7.2 1 6 1.062 9.14 us 1.091 15 KB
.NET 5.0 10 1 1.071 21.95 us 1.098 40 KB
.NET Framework 4.7.2 10 1 1.085 33.00 us 1.091 41 KB
.NET 5.0 10 2 1.076 33.81 us 1.103 61 KB
.NET Framework 4.7.2 10 2 1.071 39.70 us 1.093 61 KB
.NET 5.0 10 6 1.074 73.76 us 1.103 140 KB
.NET Framework 4.7.2 10 6 1.015 20.28 us 1.094 140 KB
.NET 5.0 100 1 1.066 200.61 us 1.099 395 KB
.NET Framework 4.7.2 100 1 1.060 228.67 us 1.090 396 KB
.NET 5.0 100 2 1.058 259.74 us 1.101 592 KB
.NET Framework 4.7.2 100 2 1.059 330.60 us 1.092 594 KB
.NET 5.0 100 6 1.069 717.99 us 1.102 1380 KB
.NET Framework 4.7.2 100 6 1.078 997.37 us 1.093 1384 KB
.NET 5.0 500 1 1.093 1434.78 us 1.099 1971 KB
.NET Framework 4.7.2 500 1 1.042 825.59 us 1.090 1976 KB
.NET 5.0 500 2 1.051 1177.92 us 1.101 2955 KB
.NET Framework 4.7.2 500 2 1.064 1850.15 us 1.092 2964 KB
.NET 5.0 500 6 1.074 3931.70 us 1.102 6892 KB
.NET Framework 4.7.2 500 6 1.068 4362.01 us 1.093 6914 KB

Conclusion

So across time and GC for this benchmark, there's a 4-10% regression.

@jnyrup
Copy link
Member

jnyrup commented Oct 20, 2021

A quick profile showed that GetEqualityStrategy accounted for 11.24% of the time.
image

Some ideas:

  • type.IsRecord can be statically cached.
  • The calls to Any allocates the delegate (as it captures type) if even the collection is referenceTypes/valueTypes are empty, which I guess they are most of the time.
  • Could we make an instance scoped Type -> EqualityStrategy cache?
    • Perhaps instead of the existing hasValueSemanticsMap cache.

@dennisdoomen
Copy link
Member Author

While profiling the 400 equivalency specs using dotTrace (and Rider's integrated profiler), I get this:

image

So it seems we have several opportunities:

  • Cache IsRecord() globally
  • Cache OverridesEqualls globally. It was called 217K times.
  • Cache GetEqualityStrategy within SelfReferenceEquivalencyAssertionOptions

@dennisdoomen
Copy link
Member Author

I'll create a follow-up PR

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.

Combination of ComparingByMember and IgnoreCyclicReferences doesn't work

2 participants