Skip to content

Optimize ImmutableSortedSet<T>.SetEquals to avoid unnecessary allocations#126549

Draft
aw0lid wants to merge 1 commit intodotnet:mainfrom
aw0lid:fix-immutableSortedset-setequals-allocs
Draft

Optimize ImmutableSortedSet<T>.SetEquals to avoid unnecessary allocations#126549
aw0lid wants to merge 1 commit intodotnet:mainfrom
aw0lid:fix-immutableSortedset-setequals-allocs

Conversation

@aw0lid
Copy link
Copy Markdown

@aw0lid aw0lid commented Apr 4, 2026

Summary

ImmutableSortedSet<T>.SetEquals always creates a new intermediate SortedSet<T> for the other collection, leading to avoidable allocations and GC pressure, especially for large datasets

Optimization Logic

  • Type-Specific Fast Paths: Uses pattern matching to detect if other is already an ImmutableSortedSet<T> or SortedSet<T>.
  • Comparer Validation: Only triggers the fast path if the Comparer matches, ensuring semantic correctness.
  • O(1) Early Exit: Checks Count immediately; if they don't match, we return false without any enumeration or allocation.
  • Zero Allocation: When a compatible set is found, we iterate directly over the existing instance.
  • Deferred Fallback: The expensive new SortedSet<T>(other) is now a last resort, used only for general IEnumerable types.
Click to expand Benchmark Source Code
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;

[MemoryDiagnoser]
[Orderer(BenchmarkDotNet.Order.SummaryOrderPolicy.FastestToSlowest)]
public class ImmutableSortedSetEqualsBenchmark
{
    private ImmutableSortedSet<int> _sourceSet = null!;
    private ImmutableSortedSet<int> _sameReference = null!;
    private ImmutableSortedSet<int> _immutableSortedMatch = null!;
    private ImmutableSortedSet<int> _immutableSortedDifferentComparer = null!;
    private SortedSet<int> _sortedSetMatch = null!;
    private SortedSet<int> _sortedSetDifferentCount = null!;
    private List<int> _listMatch = null!;
    private int[] _arrayMatch = null!;
    private HashSet<int> _hashSetMatch = null!;

    [Params(100, 10000)]
    public int Size;

    [GlobalSetup]
    public void Setup()
    {
        var elements = Enumerable.Range(0, Size).ToList();
        
        _sourceSet = ImmutableSortedSet.CreateRange(elements);
        
        _sameReference = _sourceSet;
        
        _immutableSortedMatch = ImmutableSortedSet.CreateRange(elements);
        
        _sortedSetMatch = new SortedSet<int>(elements);
        
        _sortedSetDifferentCount = new SortedSet<int>(elements.Take(Size / 2));
        
        _immutableSortedDifferentComparer = ImmutableSortedSet.CreateRange(new CustomDescendingComparer(), elements);
        
        _listMatch = elements;
        _arrayMatch = elements.ToArray();
        _hashSetMatch = new HashSet<int>(elements);
    }

    [Benchmark(Description = "Identity (Same Ref)")]
    public bool Case01_SameReference() => _sourceSet.SetEquals(_sameReference);

    [Benchmark(Description = "Opt: ImmutableSortedSet (Match)")]
    public bool Case02_ImmutableSortedMatch() => _sourceSet.SetEquals(_immutableSortedMatch);

    [Benchmark(Description = "Opt: SortedSet (Match)")]
    public bool Case03_SortedSetMatch() => _sourceSet.SetEquals(_sortedSetMatch);

    [Benchmark(Description = "Diff Count (O1 Check)")]
    public bool Case04_DifferentCount() => _sourceSet.SetEquals(_sortedSetDifferentCount);

    [Benchmark(Description = "Fallback: List (New SortedSet)")]
    public bool Case05_ListMatch() => _sourceSet.SetEquals(_listMatch);

    [Benchmark(Description = "Fallback: Array (New SortedSet)")]
    public bool Case06_ArrayMatch() => _sourceSet.SetEquals(_arrayMatch);

    [Benchmark(Description = "Fallback: HashSet (New SortedSet)")]
    public bool Case07_HashSetMatch() => _sourceSet.SetEquals(_hashSetMatch);

    [Benchmark(Description = "Fallback: Diff Comparer")]
    public bool Case08_DifferentComparer() => _sourceSet.SetEquals(_immutableSortedDifferentComparer);
}

public class CustomDescendingComparer : IComparer<int>
{
    public int Compare(int x, int y) => y.CompareTo(x);
}

public class Program
{
    public static void Main(string[] args) => BenchmarkRunner.Run<ImmutableSortedSetEqualsBenchmark>();
}
Click to expand Benchmark Results

Benchmark Results (Before Optimization)

Method Size Mean Error StdDev Gen0 Gen1 Allocated
'Identity (Same Ref)' 100 0.9946 ns 0.0498 ns 0.0416 ns - - -
'Diff Count (O1 Check)' 100 1,060.6103 ns 45.1062 ns 132.2886 ns 1.4629 - 2296 B
'Opt: SortedSet (Match)' 100 3,904.3241 ns 64.1496 ns 78.7815 ns 2.8534 - 4480 B
'Fallback: List (New SortedSet)' 100 3,958.3857 ns 78.7675 ns 93.7671 ns 2.9449 - 4624 B
'Fallback: Array (New SortedSet)' 100 4,025.4247 ns 60.0027 ns 130.4409 ns 2.9449 - 4624 B
'Fallback: HashSet (New SortedSet)' 100 4,179.5471 ns 58.5189 ns 127.2153 ns 2.9449 - 4624 B
'Opt: ImmutableSortedSet (Match)' 100 6,853.6154 ns 134.8278 ns 112.5873 ns 2.9449 - 4624 B
'Fallback: Diff Comparer' 100 7,220.0200 ns 119.6871 ns 175.4358 ns 2.9449 - 4624 B
'Identity (Same Ref)' 10000 0.6522 ns 0.0625 ns 0.0585 ns - - -
'Diff Count (O1 Check)' 10000 143,729.0354 ns 5,602.2129 ns 16,518.2522 ns 72.9980 22.9492 200520 B
'Fallback: Array (New SortedSet)' 10000 1,065,894.3900 ns 14,465.4279 ns 12,079.2874 ns 99.6094 62.5000 440337 B
'Fallback: List (New SortedSet)' 10000 1,093,057.7746 ns 20,174.6286 ns 31,999.1015 ns 99.6094 60.5469 440337 B
'Opt: SortedSet (Match)' 10000 1,109,861.5537 ns 19,740.4865 ns 22,733.1736 ns 99.6094 50.7813 400816 B
'Fallback: HashSet (New SortedSet)' 10000 1,112,267.9964 ns 16,153.0182 ns 15,109.5432 ns 101.5625 60.5469 440336 B
'Opt: ImmutableSortedSet (Match)' 10000 1,423,228.8945 ns 28,269.4857 ns 54,465.7057 ns 99.6094 58.5938 440336 B
'Fallback: Diff Comparer' 10000 1,487,248.8424 ns 17,175.9974 ns 15,226.0743 ns 101.5625 62.5000 440337 B

Benchmark Results (After Optimization)

Method Size Mean Error StdDev Gen0 Gen1 Allocated
'Identity (Same Ref)' 100 1.518 ns 0.0724 ns 0.0677 ns - - -
'Diff Count (O1 Check)' 100 3.423 ns 0.0455 ns 0.0380 ns - - -
'Opt: SortedSet (Match)' 100 2,115.911 ns 13.9706 ns 12.3846 ns 0.0954 - 152 B
'Opt: ImmutableSortedSet (Match)' 100 2,244.976 ns 30.4929 ns 28.5231 ns - - -
'Fallback: List (New SortedSet)' 100 4,101.207 ns 35.1979 ns 32.9242 ns 2.9449 - 4624 B
'Fallback: Array (New SortedSet)' 100 4,187.807 ns 44.6459 ns 41.7618 ns 2.9449 - 4624 B
'Fallback: HashSet (New SortedSet)' 100 4,511.521 ns 53.3641 ns 49.9168 ns 2.9449 - 4624 B
'Fallback: Diff Comparer' 100 5,468.477 ns 42.0684 ns 37.2925 ns 2.9449 - 4624 B
'Identity (Same Ref)' 10000 1.666 ns 0.0467 ns 0.0390 ns - - -
'Diff Count (O1 Check)' 10000 3.586 ns 0.0499 ns 0.0442 ns - - -
'Opt: SortedSet (Match)' 10000 736,895.117 ns 4,762.4595 ns 3,976.8694 ns - - 264 B
'Opt: ImmutableSortedSet (Match)' 10000 876,700.334 ns 4,883.3320 ns 4,567.8718 ns - - -
'Fallback: Array (New SortedSet)' 10000 1,075,253.325 ns 9,913.5548 ns 8,788.1081 ns 95.7031 58.5938 440337 B
'Fallback: List (New SortedSet)' 10000 1,085,035.913 ns 16,843.6982 ns 14,065.2508 ns 101.5625 62.5000 440337 B
'Fallback: HashSet (New SortedSet)' 10000 1,106,170.194 ns 10,647.5821 ns 9,959.7549 ns 103.5156 60.5469 440336 B
'Fallback: Diff Comparer' 10000 1,330,945.921 ns 12,912.2723 ns 12,078.1475 ns 103.5156 62.5000 440336 B

Performance Analysis (10,000 Elements)

  • Identity (Same Ref): Execution time remained ultra-fast and stable (~1.6 ns), confirming that the new validation logic introduces no measurable overhead to the most direct comparison path.
  • Diff Count (O(1) Check): A massive breakthrough; execution speed increased by approximately 40,080x (dropping from 143,729 ns to 3.58 ns). Furthermore, heap allocation was completely eliminated (from 200,520 B to 0 B), representing a 100% improvement in memory efficiency.
  • Type: ImmutableSortedSet (Match): Significant performance gain with a 38.4% reduction in execution time (from 1.42 ms to 0.87 ms). Crucially, memory allocation was entirely eliminated (from 440,336 B to 0 B), removing all pressure from the Garbage Collector (GC).
  • Type: SortedSet (Match): Notable speed increase of 33.6% (from 1.10 ms to 0.73 ms). Memory allocation dropped by 99.93% (from 400,816 B to only 264 B), effectively reaching a near-zero allocation state.
  • Fallback: Array / List / HashSet: These scenarios remained stable with minor speed improvements of 1% to 2%. Memory allocation stayed consistent (~440 KB), ensuring zero regressions for types that do not qualify for the fast path.
  • Fallback: Diff Comparer: Execution time improved by 10.5% (from 1.48 ms to 1.33 ms) while maintaining stable memory usage. This confirms that the comparer validation correctly routes operations without penalizing the overall process.

@dotnet-policy-service dotnet-policy-service bot added the community-contribution Indicates that the PR has been added by a community member label Apr 4, 2026
@dotnet-policy-service
Copy link
Copy Markdown
Contributor

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

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.

1 participant