Skip to content

Conversation

prozolic
Copy link

@prozolic prozolic commented Aug 20, 2025

Summary

This PR improves the performance of ImmutableArrayExtensions.SequenceEqual method through optimized implementation strategies:

Key Performance Improvements:

  • Array: ~91% faster (64.31ns → 5.671ns)
  • List: ~93% faster (87.06ns → 6.130ns)
  • IList: ~49% faster (105.71ns → 53.489ns)
  • ICollection: No statistically significant performance difference
  • IEnumerable: Uses existing implementation for this case because the existing implementation had better performance and didn't allocate a second enumerator

Implementation Approach

The implementation was refined based on review feedback from @neon-sunset and @xtqqczze to achieve optimal performance while balancing complexity:

  1. Fast path for common collections: Detects when items implement ICollection<T> and delegates to the highly optimized Enumerable.SequenceEqual using the underlying array
  2. Compatibility preservation: Falls back to existing implementation for other IEnumerable<T> types to maintain behavioral consistency

Benchmark (dotnet/performance#4915)

Original:

| Method        | input       | Mean      | Error    | StdDev   | Median    | Min       | Max       | Gen0   | Allocated |
|-------------- |------------ |----------:|---------:|---------:|----------:|----------:|----------:|-------:|----------:|
| SequenceEqual | Array       |  64.31 ns | 0.841 ns | 0.787 ns |  64.06 ns |  63.53 ns |  66.34 ns |      - |         - |
| SequenceEqual | ICollection | 106.56 ns | 2.078 ns | 2.041 ns | 106.17 ns | 104.46 ns | 110.28 ns | 0.0036 |      32 B |
| SequenceEqual | IEnumerable | 108.40 ns | 2.116 ns | 1.979 ns | 108.15 ns | 105.81 ns | 111.40 ns | 0.0035 |      32 B |
| SequenceEqual | IList       | 105.71 ns | 1.733 ns | 1.621 ns | 105.44 ns | 104.01 ns | 108.46 ns | 0.0035 |      32 B |
| SequenceEqual | List        |  87.06 ns | 3.531 ns | 4.066 ns |  85.22 ns |  84.66 ns | 100.81 ns |      - |         - |

Modified:

| Method        | input       | Mean       | Error     | StdDev    | Median     | Min        | Max        | Gen0   | Allocated |
|-------------- |------------ |-----------:|----------:|----------:|-----------:|-----------:|-----------:|-------:|----------:|
| SequenceEqual | Array       |   5.671 ns | 0.3384 ns | 0.3897 ns |   5.544 ns |   5.419 ns |   7.055 ns |      - |         - |
| SequenceEqual | ICollection | 103.143 ns | 1.9016 ns | 1.7788 ns | 102.466 ns | 101.597 ns | 107.242 ns | 0.0037 |      32 B |
| SequenceEqual | IEnumerable | 107.665 ns | 2.1441 ns | 2.3832 ns | 106.737 ns | 105.110 ns | 113.926 ns | 0.0034 |      32 B |
| SequenceEqual | IList       |  53.489 ns | 0.1662 ns | 0.1555 ns |  53.459 ns |  53.242 ns |  53.848 ns |      - |         - |
| SequenceEqual | List        |   6.130 ns | 0.3136 ns | 0.3612 ns |   5.973 ns |   5.922 ns |   7.384 ns |      - |         - |

@github-actions github-actions bot added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Aug 20, 2025
@dotnet-policy-service dotnet-policy-service bot added the community-contribution Indicates that the PR has been added by a community member label Aug 20, 2025
@prozolic
Copy link
Author

@dotnet-policy-service agree

@jkotas jkotas added area-System.Collections and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Aug 20, 2025
Copy link
Contributor

Tagging subscribers to this area: @dotnet/area-system-collections
See info in area-owners.md if you want to be subscribed.

@neon-sunset
Copy link
Contributor

Perhaps a simpler (and faster) way to do this is to just call ImmutableCollectionsMarshal.AsArray and delegate to already optimized Enumerable.SequenceEqual by using the underlying array instead?

…utableArrayExtensions.cs

Co-authored-by: Pranav Senthilnathan <[email protected]>
@prozolic
Copy link
Author

I'm not sure if it can be applied to ImmutableArrayExtensions.SequenceEqual, but I also think that is the simplest and fastest approach.

@xtqqczze
Copy link
Contributor

xtqqczze commented Aug 22, 2025

I'm not sure if it can be applied to ImmutableArrayExtensions.SequenceEqual, but I also think that is the simplest and fastest approach.

This should work:

Requires.NotNull(items, nameof(items));
immutableArray.ThrowNullRefIfNotInitialized();
return immutableArray.array.SequenceEqual((IEnumerable<TBase>)items, comparer);

If it is acceptable to delegate the argument validation to Enumerable.SequenceEqual, this can be simplified to just:

return immutableArray.array.SequenceEqual((IEnumerable<TBase>)items, comparer);

@prozolic
Copy link
Author

@xtqqczze
Thank you!. The performance is also better than what I originally proposed, so I've changed the implementation to this approach.

Full benchmark results
| Method                       | Size | Mean         | Error      | StdDev     | Gen0   | Allocated |
|----------------------------- |----- |-------------:|-----------:|-----------:|-------:|----------:|
| SequenceEqual_Sequence       | 10   |    29.430 ns |  0.5630 ns |  0.4991 ns | 0.0047 |      40 B |
| SequenceEqual_Array          | 10   |     6.133 ns |  0.0724 ns |  0.0642 ns |      - |         - |
| SequenceEqual_List           | 10   |    10.141 ns |  1.3583 ns |  1.5097 ns |      - |         - |
| SequenceEqual_Sequence2      | 10   |    41.513 ns |  4.9371 ns |  5.6856 ns | 0.0047 |      40 B |
| SequenceEqual_Array2         | 10   |     2.002 ns |  0.4888 ns |  0.5629 ns |      - |         - |
| SequenceEqual_List2          | 10   |     1.498 ns |  0.3371 ns |  0.3883 ns |      - |         - |
| Linq_SequenceEqual_Sequence  | 10   |    38.917 ns |  1.2823 ns |  1.4767 ns | 0.0085 |      72 B |
| Linq_SequenceEqual_Array     | 10   |     3.958 ns |  0.1180 ns |  0.1359 ns |      - |         - |
| Linq_SequenceEqual_List      | 10   |     4.057 ns |  0.1381 ns |  0.1477 ns |      - |         - |
| Linq_SequenceEqual_Sequence2 | 10   |    41.680 ns |  1.2026 ns |  1.3849 ns | 0.0085 |      72 B |
| Linq_SequenceEqual_Array2    | 10   |     2.387 ns |  0.1295 ns |  0.1491 ns |      - |         - |
| Linq_SequenceEqual_List2     | 10   |     2.550 ns |  0.2062 ns |  0.2206 ns |      - |         - |
| SequenceEqual_Sequence       | 100  |   200.385 ns |  4.0023 ns |  4.1100 ns | 0.0040 |      40 B |
| SequenceEqual_Array          | 100  |    46.223 ns |  0.3622 ns |  0.3024 ns |      - |         - |
| SequenceEqual_List           | 100  |    61.303 ns |  1.1493 ns |  1.0188 ns |      - |         - |
| SequenceEqual_Sequence2      | 100  |   246.445 ns |  1.3563 ns |  1.2687 ns | 0.0040 |      40 B |
| SequenceEqual_Array2         | 100  |     1.598 ns |  0.1338 ns |  0.1432 ns |      - |         - |
| SequenceEqual_List2          | 100  |     1.520 ns |  0.0316 ns |  0.0280 ns |      - |         - |
| Linq_SequenceEqual_Sequence  | 100  |   245.714 ns |  1.7712 ns |  1.6568 ns | 0.0079 |      72 B |
| Linq_SequenceEqual_Array     | 100  |     6.832 ns |  0.1569 ns |  0.1468 ns |      - |         - |
| Linq_SequenceEqual_List      | 100  |     8.093 ns |  0.2206 ns |  0.2361 ns |      - |         - |
| Linq_SequenceEqual_Sequence2 | 100  |   261.690 ns | 12.1481 ns | 12.4752 ns | 0.0078 |      72 B |
| Linq_SequenceEqual_Array2    | 100  |     2.384 ns |  0.1026 ns |  0.1182 ns |      - |         - |
| Linq_SequenceEqual_List2     | 100  |     2.801 ns |  0.3324 ns |  0.3828 ns |      - |         - |
| SequenceEqual_Sequence       | 1000 | 1,847.869 ns | 27.8300 ns | 26.0322 ns |      - |      40 B |
| SequenceEqual_Array          | 1000 |   375.823 ns |  2.3138 ns |  1.9321 ns |      - |         - |
| SequenceEqual_List           | 1000 |   554.672 ns |  5.0257 ns |  4.1967 ns |      - |         - |
| SequenceEqual_Sequence2      | 1000 | 1,606.066 ns | 28.1198 ns | 23.4813 ns |      - |      40 B |
| SequenceEqual_Array2         | 1000 |     1.504 ns |  0.0456 ns |  0.0426 ns |      - |         - |
| SequenceEqual_List2          | 1000 |     1.335 ns |  0.0504 ns |  0.0539 ns |      - |         - |
| Linq_SequenceEqual_Sequence  | 1000 | 2,201.701 ns | 20.5064 ns | 18.1784 ns |      - |      72 B |
| Linq_SequenceEqual_Array     | 1000 |    36.951 ns |  1.4001 ns |  1.4378 ns |      - |         - |
| Linq_SequenceEqual_List      | 1000 |    36.993 ns |  1.5750 ns |  1.7507 ns |      - |         - |
| Linq_SequenceEqual_Sequence2 | 1000 | 2,243.976 ns | 44.6613 ns | 41.7762 ns |      - |      72 B |
| Linq_SequenceEqual_Array2    | 1000 |     2.720 ns |  0.3530 ns |  0.4065 ns |      - |         - |
| Linq_SequenceEqual_List2     | 1000 |     2.484 ns |  0.2212 ns |  0.2458 ns |      - |         - |

@xtqqczze
Copy link
Contributor

The performance is also better than what I originally proposed, so I've changed the implementation to this approach.

@prozolic I see a few regressions. Could you collect benchmark data for https://github.com/xtqqczze/dotnet-runtime/tree/ImmutableArrayExtensions.SequenceEqual?. This approach will not be worth the additional complexity, however.

@prozolic
Copy link
Author

prozolic commented Aug 22, 2025

Sorry. Indeed, there were memory allocation issues and regressions in some cases. I conducted performance measurements comparing different implementations of SequenceEqual.

  • SequenceEqual_Original: Current existing implementation
  • SequenceEqual_1: My initially proposed implementation
  • SequenceEqual_2: @xtqqczze's implementation approach
  • SequenceEqual_3: Implementation using Enumerable.SequenceEqual
Full benchmark results
| Method                          | Size | Mean         | Error      | StdDev     | Median       | Min          | Max          | Gen0   | Allocated |
|-------------------------------- |----- |-------------:|-----------:|-----------:|-------------:|-------------:|-------------:|-------:|----------:|
| SequenceEqual_Original_Sequence | 10   |    29.367 ns |  0.6282 ns |  0.6982 ns |    29.062 ns |    28.659 ns |    30.968 ns | 0.0047 |      40 B |
| SequenceEqual_Original_Array    | 10   |     7.832 ns |  0.2126 ns |  0.2448 ns |     7.759 ns |     7.563 ns |     8.346 ns |      - |         - |
| SequenceEqual_Original_List     | 10   |    10.526 ns |  0.1244 ns |  0.1103 ns |    10.479 ns |    10.440 ns |    10.755 ns |      - |         - |
| SequenceEqual_1_Sequence        | 10   |    29.635 ns |  0.7748 ns |  0.8290 ns |    29.291 ns |    28.906 ns |    31.585 ns | 0.0047 |      40 B |
| SequenceEqual_1_Array           | 10   |     6.404 ns |  0.1304 ns |  0.1219 ns |     6.357 ns |     6.278 ns |     6.694 ns |      - |         - |
| SequenceEqual_1_List            | 10   |     8.755 ns |  0.1162 ns |  0.0970 ns |     8.706 ns |     8.612 ns |     8.915 ns |      - |         - |
| SequenceEqual_2_Sequence        | 10   |    32.641 ns |  0.5341 ns |  0.4735 ns |    32.537 ns |    32.014 ns |    33.533 ns | 0.0047 |      40 B |
| SequenceEqual_2_Array           | 10   |     3.557 ns |  0.0850 ns |  0.0710 ns |     3.515 ns |     3.497 ns |     3.709 ns |      - |         - |
| SequenceEqual_2_List            | 10   |     2.711 ns |  0.0300 ns |  0.0266 ns |     2.702 ns |     2.687 ns |     2.772 ns |      - |         - |
| SequenceEqual_3_Sequence        | 10   |    36.971 ns |  0.5349 ns |  0.4742 ns |    36.810 ns |    36.374 ns |    38.207 ns | 0.0085 |      72 B |
| SequenceEqual_3_Array           | 10   |     4.584 ns |  0.0485 ns |  0.0430 ns |     4.566 ns |     4.545 ns |     4.684 ns |      - |         - |
| SequenceEqual_3_List            | 10   |     3.958 ns |  0.0137 ns |  0.0107 ns |     3.957 ns |     3.942 ns |     3.979 ns |      - |         - |
| SequenceEqual_Original_Sequence | 100  |   196.088 ns |  1.4928 ns |  1.1655 ns |   196.009 ns |   194.441 ns |   198.118 ns | 0.0047 |      40 B |
| SequenceEqual_Original_Array    | 100  |    64.351 ns |  0.4256 ns |  0.3554 ns |    64.235 ns |    64.013 ns |    65.284 ns |      - |         - |
| SequenceEqual_Original_List     | 100  |    84.848 ns |  1.1737 ns |  1.0979 ns |    84.272 ns |    83.994 ns |    87.165 ns |      - |         - |
| SequenceEqual_1_Sequence        | 100  |   196.206 ns |  0.8333 ns |  0.7387 ns |   196.087 ns |   195.027 ns |   197.763 ns | 0.0040 |      40 B |
| SequenceEqual_1_Array           | 100  |    46.198 ns |  0.1243 ns |  0.1038 ns |    46.171 ns |    46.036 ns |    46.408 ns |      - |         - |
| SequenceEqual_1_List            | 100  |    60.603 ns |  0.1870 ns |  0.1460 ns |    60.550 ns |    60.443 ns |    60.870 ns |      - |         - |
| SequenceEqual_2_Sequence        | 100  |   195.790 ns |  0.8383 ns |  0.7000 ns |   195.679 ns |   194.953 ns |   197.149 ns | 0.0047 |      40 B |
| SequenceEqual_2_Array           | 100  |     6.371 ns |  0.1426 ns |  0.1264 ns |     6.353 ns |     6.161 ns |     6.576 ns |      - |         - |
| SequenceEqual_2_List            | 100  |     5.360 ns |  0.0521 ns |  0.0435 ns |     5.342 ns |     5.324 ns |     5.482 ns |      - |         - |
| SequenceEqual_3_Sequence        | 100  |   243.019 ns |  1.9271 ns |  1.7083 ns |   242.432 ns |   241.329 ns |   247.212 ns | 0.0078 |      72 B |
| SequenceEqual_3_Array           | 100  |     6.411 ns |  0.0080 ns |  0.0063 ns |     6.411 ns |     6.400 ns |     6.422 ns |      - |         - |
| SequenceEqual_3_List            | 100  |     6.652 ns |  0.0175 ns |  0.0164 ns |     6.653 ns |     6.629 ns |     6.686 ns |      - |         - |
| SequenceEqual_Original_Sequence | 1000 | 1,818.861 ns | 10.4188 ns |  8.1343 ns | 1,817.715 ns | 1,810.100 ns | 1,835.355 ns |      - |      40 B |
| SequenceEqual_Original_Array    | 1000 |   645.510 ns |  1.0769 ns |  0.9547 ns |   645.590 ns |   643.858 ns |   646.983 ns |      - |         - |
| SequenceEqual_Original_List     | 1000 | 1,295.138 ns |  4.3377 ns |  3.3866 ns | 1,294.743 ns | 1,292.376 ns | 1,305.142 ns |      - |      40 B |
| SequenceEqual_1_Sequence        | 1000 | 1,594.272 ns | 17.2476 ns | 15.2896 ns | 1,591.854 ns | 1,574.525 ns | 1,627.906 ns |      - |      40 B |
| SequenceEqual_1_Array           | 1000 |   374.425 ns |  1.1887 ns |  0.9926 ns |   374.156 ns |   373.293 ns |   376.571 ns |      - |         - |
| SequenceEqual_1_List            | 1000 |   548.014 ns |  1.5888 ns |  1.4084 ns |   547.626 ns |   546.568 ns |   551.367 ns |      - |         - |
| SequenceEqual_2_Sequence        | 1000 | 1,819.163 ns | 11.5486 ns |  9.6436 ns | 1,814.556 ns | 1,809.856 ns | 1,837.792 ns |      - |      40 B |
| SequenceEqual_2_Array           | 1000 |    34.652 ns |  0.0900 ns |  0.0752 ns |    34.639 ns |    34.563 ns |    34.813 ns |      - |         - |
| SequenceEqual_2_List            | 1000 |    34.752 ns |  0.0777 ns |  0.0606 ns |    34.743 ns |    34.675 ns |    34.846 ns |      - |         - |
| SequenceEqual_3_Sequence        | 1000 | 2,195.755 ns | 12.7553 ns | 10.6512 ns | 2,194.216 ns | 2,184.508 ns | 2,224.043 ns |      - |      72 B |
| SequenceEqual_3_Array           | 1000 |    34.630 ns |  0.3921 ns |  0.3476 ns |    34.461 ns |    34.343 ns |    35.497 ns |      - |         - |
| SequenceEqual_3_List            | 1000 |    35.973 ns |  0.5714 ns |  0.4771 ns |    35.882 ns |    35.506 ns |    37.247 ns |      - |         - |
#if NET6_0_OR_GREATER

        public static bool SequenceEqual_2<TDerived, TBase>(this ImmutableArray<TBase> immutableArray, IEnumerable<TDerived> items, IEqualityComparer<TBase>? comparer = null) where TDerived : TBase
        {
            immutableArray.ThrowNullRefIfNotInitialized();
            Requires.NotNull(items, nameof(items));

            int i = 0;

            if (items is ICollection<TBase> itemsCol)
            {
                if (itemsCol.TryGetSpan(out ReadOnlySpan<TBase> itemsSpan))
                {
                    return immutableArray.array!.SequenceEqual(itemsSpan, comparer);
                }

                if (immutableArray.array!.Length != itemsCol.Count)
                {
                    return false;
                }

                if (itemsCol is IList<TDerived> itemsList)
                {
                    comparer ??= EqualityComparer<TBase>.Default;

                    int count = immutableArray.array!.Length;
                    for (i = 0; i < count; i++)
                    {
                        if (!comparer.Equals(immutableArray.array![i], itemsList[i]))
                        {
                            return false;
                        }
                    }

                    return true;
                }
            }

            comparer ??= EqualityComparer<TBase>.Default;

            int n = immutableArray.array!.Length;
            foreach (TDerived item in items)
            {
                if (i == n)
                {
                    return false;
                }

                if (!comparer.Equals(immutableArray.array![i], item))
                {
                    return false;
                }

                i++;
            }

            return i == n;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)] // fast type checks that don't add a lot of overhead
        private static bool TryGetSpan<TSource>(this IEnumerable<TSource> source, out ReadOnlySpan<TSource> span)
        {
            // Use `GetType() == typeof(...)` rather than `is` to avoid cast helpers.  This is measurably cheaper
            // but does mean we could end up missing some rare cases where we could get a span but don't (e.g. a uint[]
            // masquerading as an int[]).  That's an acceptable tradeoff.  The Unsafe usage is only after we've
            // validated the exact type; this could be changed to a cast in the future if the JIT starts to recognize it.
            // We only pay the comparison/branching costs here for super common types we expect to be used frequently
            // with LINQ methods.

            bool result = true;
            if (source.GetType() == typeof(TSource[]))
            {
                span = Unsafe.As<TSource[]>(source);
            }
            else if (source.GetType() == typeof(List<TSource>))
            {
                span = CollectionsMarshal.AsSpan(Unsafe.As<List<TSource>>(source));
            }
            else
            {
                span = default;
                result = false;
            }

            return result;
        }

#endif

@xtqqczze
Copy link
Contributor

Sorry. Indeed, there were memory allocation issues and regressions in some cases. I conducted performance measurements comparing different implementations of SequenceEqual.

OK, so using Enumerable.SequenceEqual directly, there is an extra enumerator allocation when items is not a collection.

I think the best balance of complexity to performance would be to delegate to Enumerable.SequenceEqual only if items is type ICollection<T>, otherwise use the existing implementation.

@xtqqczze
Copy link
Contributor

Something like:

        {
            if (items is ICollection<TBase> itemsCol)
            {
                immutableArray.ThrowNullRefIfNotInitialized();
                return immutableArray.array!.SequenceEqual(itemsCol, comparer);
            }
            
            return Impl(immutableArray, items, comparer);
        }

and then put argument validation and existing implementation in static local function Impl.

@prozolic
Copy link
Author

prozolic commented Aug 22, 2025

Thank you for the advice!
If there are no issues, I'll recommit with these changes.

        public static bool SequenceEqual<TDerived, TBase>(this ImmutableArray<TBase> immutableArray, IEnumerable<TDerived> items, IEqualityComparer<TBase>? comparer = null) where TDerived : TBase
        {
            if (items is ICollection<TBase> itemsCol)
            {
                immutableArray.ThrowNullRefIfNotInitialized();
                return immutableArray.array!.SequenceEqual(itemsCol, comparer);
            }

            return Impl(immutableArray, items, comparer);

            static bool Impl(ImmutableArray<TBase> immutableArray, IEnumerable<TDerived> items, IEqualityComparer<TBase>? comparer)
            {
                Requires.NotNull(items, nameof(items));

                comparer ??= EqualityComparer<TBase>.Default;

                int i = 0;
                int n = immutableArray.Length;
                foreach (TDerived item in items)
                {
                    if (i == n)
                    {
                        return false;
                    }

                    if (!comparer.Equals(immutableArray[i], item))
                    {
                        return false;
                    }

                    i++;
                }

                return i == n;
            }
        }

@xtqqczze
Copy link
Contributor

@prozolic You don't need the comparer = null in the Impl signature.

Could you also provide benchmark data again?

@prozolic
Copy link
Author

@xtqqczze Here are the benchmark results comparing with the current existing implementation (SequenceEqual_Original):

| Method                          | Size | Mean         | Error      | StdDev     | Median       | Min          | Max          | Gen0   | Allocated |
|-------------------------------- |----- |-------------:|-----------:|-----------:|-------------:|-------------:|-------------:|-------:|----------:|
| SequenceEqual_Original_Sequence | 10   |    32.759 ns |  2.9278 ns |  3.1327 ns |    32.048 ns |    29.604 ns |    40.901 ns | 0.0047 |      40 B |
| SequenceEqual_Original_Array    | 10   |     7.886 ns |  0.1893 ns |  0.2104 ns |     7.818 ns |     7.608 ns |     8.256 ns |      - |         - |
| SequenceEqual_Original_List     | 10   |    10.709 ns |  0.2367 ns |  0.2325 ns |    10.699 ns |    10.438 ns |    11.160 ns |      - |         - |
| SequenceEqual_Sequence          | 10   |    29.727 ns |  0.6673 ns |  0.7140 ns |    29.684 ns |    28.867 ns |    31.021 ns | 0.0047 |      40 B |
| SequenceEqual_Array             | 10   |     3.704 ns |  0.5038 ns |  0.5802 ns |     3.477 ns |     3.007 ns |     4.748 ns |      - |         - |
| SequenceEqual_List              | 10   |     3.410 ns |  0.1944 ns |  0.2080 ns |     3.347 ns |     3.196 ns |     3.946 ns |      - |         - |
| SequenceEqual_Original_Sequence | 100  |   197.313 ns |  2.6539 ns |  2.3526 ns |   196.760 ns |   193.830 ns |   201.666 ns | 0.0043 |      40 B |
| SequenceEqual_Original_Array    | 100  |    66.773 ns |  2.8033 ns |  2.9995 ns |    65.365 ns |    64.431 ns |    73.763 ns |      - |         - |
| SequenceEqual_Original_List     | 100  |    92.793 ns |  5.7551 ns |  6.6276 ns |    91.450 ns |    84.552 ns |   109.164 ns |      - |         - |
| SequenceEqual_Sequence          | 100  |   201.575 ns |  4.7906 ns |  5.1259 ns |   200.430 ns |   195.562 ns |   215.626 ns | 0.0047 |      40 B |
| SequenceEqual_Array             | 100  |     6.953 ns |  0.0702 ns |  0.0548 ns |     6.950 ns |     6.889 ns |     7.069 ns |      - |         - |
| SequenceEqual_List              | 100  |     7.090 ns |  0.3510 ns |  0.3755 ns |     6.930 ns |     6.736 ns |     8.100 ns |      - |         - |
| SequenceEqual_Original_Sequence | 1000 | 1,840.548 ns | 22.7347 ns | 21.2661 ns | 1,835.535 ns | 1,816.222 ns | 1,877.762 ns |      - |      40 B |
| SequenceEqual_Original_Array    | 1000 |   650.695 ns |  3.7116 ns |  3.0994 ns |   649.948 ns |   646.472 ns |   655.441 ns |      - |         - |
| SequenceEqual_Original_List     | 1000 | 1,302.943 ns |  4.4122 ns |  3.9113 ns | 1,302.234 ns | 1,297.050 ns | 1,311.717 ns |      - |      40 B |
| SequenceEqual_Sequence          | 1000 | 1,861.443 ns | 43.0806 ns | 44.2406 ns | 1,839.284 ns | 1,819.619 ns | 1,982.379 ns |      - |      40 B |
| SequenceEqual_Array             | 1000 |    36.563 ns |  0.3177 ns |  0.2653 ns |    36.451 ns |    36.222 ns |    37.124 ns |      - |         - |
| SequenceEqual_List              | 1000 |    36.467 ns |  0.2840 ns |  0.2657 ns |    36.480 ns |    36.092 ns |    36.840 ns |      - |         - |

@xtqqczze
Copy link
Contributor

@prozolic The results look excellent, very close to SequenceEqual_2, but achieved with significantly lower complexity.

@xtqqczze
Copy link
Contributor

It doesn’t look like we currently have performance coverage for this method in https://github.com/dotnet/performance.

@prozolic
Copy link
Author

You're right, there's no official performance test coverage for this yet. From what I can see, there isn't much performance test coverage for ImmutableArray itself in dotnet/performance either.

@prozolic
Copy link
Author

Regarding the benchmark, I submitted a pull request at dotnet/performance#4915.
The results are as follows:

Original:

| Method        | input       | Mean      | Error    | StdDev   | Median    | Min       | Max       | Gen0   | Allocated |
|-------------- |------------ |----------:|---------:|---------:|----------:|----------:|----------:|-------:|----------:|
| SequenceEqual | Array       |  70.16 ns | 6.792 ns | 7.822 ns |  65.40 ns |  63.45 ns |  89.38 ns |      - |         - |
| SequenceEqual | ICollection | 107.19 ns | 2.487 ns | 2.864 ns | 107.12 ns | 103.84 ns | 114.27 ns | 0.0034 |      32 B |
| SequenceEqual | IEnumerable | 108.12 ns | 2.173 ns | 2.032 ns | 108.05 ns | 105.38 ns | 114.04 ns | 0.0038 |      32 B |
| SequenceEqual | IList       | 112.49 ns | 6.861 ns | 7.901 ns | 109.21 ns | 104.19 ns | 131.30 ns | 0.0035 |      32 B |

Modified:

| Method        | input       | Mean       | Error      | StdDev     | Median     | Min        | Max        | Gen0   | Allocated |
|-------------- |------------ |-----------:|-----------:|-----------:|-----------:|-----------:|-----------:|-------:|----------:|
| SequenceEqual | Array       |   6.127 ns |  0.3179 ns |  0.3661 ns |   6.007 ns |   5.677 ns |   6.938 ns |      - |         - |
| SequenceEqual | ICollection | 110.221 ns | 15.7634 ns | 18.1532 ns | 102.911 ns | 101.468 ns | 176.069 ns | 0.0038 |      32 B |
| SequenceEqual | IEnumerable | 113.054 ns |  7.7148 ns |  8.8844 ns | 112.866 ns | 104.780 ns | 146.576 ns | 0.0036 |      32 B |
| SequenceEqual | IList       |  54.985 ns |  3.4532 ns |  3.9767 ns |  53.409 ns |  52.861 ns |  69.285 ns |      - |         - |

@xtqqczze
Copy link
Contributor

@prozolic Does PR description need updating with new performance numbers?

@prozolic
Copy link
Author

@xtqqczze
Yes, we need to update the PR description. The current description contains old benchmark results based on my initial approach, but the actual implementation and performance have changed significantly through review and feedback from @neon-sunset and @xtqqczze.

Therefore, I have updated the Summary description to match the current implementation and changed the benchmark results to those from dotnet/performance#4915.

@xtqqczze
Copy link
Contributor

  • ICollection: ~17% faster (123.20ns → 102.126ns), eliminated allocations for some cases

We cannot make this conclusion from the data, as there is high Error/StdDev for the ICollection benchmark. Were other applications running at the same time as the benchmark?

IEnumerable: Maintained similar performance while preserving compatibility

We didn't delegate to Enumerable.SequenceEqual for this case because the existing implementation had better performance and didn't allocate a second enumerator.

@prozolic
Copy link
Author

We cannot make this conclusion from the data, as there is high Error/StdDev for the ICollection benchmark. Were other applications running at the same time as the benchmark?

You're right about the ICollection claim. I've updated it to "No statistically significant performance difference" (please let me know if there's a better wording). Regarding benchmark results, other applications may have been running and affecting the measurements, so I re-ran the benchmarks in a clean environment and updated the results.

We didn't delegate to Enumerable.SequenceEqual for this case because the existing implementation had better performance and didn't allocate a second enumerator.

I've updated it to "Uses existing implementation for this case because the existing implementation had better performance and didn't allocate a second enumerator"

int i = 0;
int n = immutableArray.Length;
foreach (TDerived item in items)
static bool Impl(ImmutableArray<TBase> immutableArray, IEnumerable<TDerived> items, IEqualityComparer<TBase>? comparer)
Copy link
Contributor

Choose a reason for hiding this comment

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

I think you can change the type of the items parameter here to IEnumerable<TBase> and cast at the call site. Let's see what other reviewers think.

Copy link
Author

Choose a reason for hiding this comment

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

I don't think there would be much difference in complexity or performance compared to the current implementation, so I personally think we don't particularly need to make the change.

Copy link
Contributor

Choose a reason for hiding this comment

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

There may be a difference on R2R due to boxing, I'm not sure.

Copy link
Author

Choose a reason for hiding this comment

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

I'm also not very familiar with R2R (ReadyToRun), so I'm sorry but I don't know the impact of this change either.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-System.Collections community-contribution Indicates that the PR has been added by a community member
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants