Skip to content

Commit f86ad5b

Browse files
committed
Optimize ImmutableSortedSet<T>.SetEquals to avoid unnecessary allocations
1 parent 4ff20e6 commit f86ad5b

2 files changed

Lines changed: 170 additions & 12 deletions

File tree

src/libraries/System.Collections.Immutable/src/System/Collections/Immutable/ImmutableSortedSet_1.cs

Lines changed: 80 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -375,24 +375,56 @@ public bool SetEquals(IEnumerable<T> other)
375375
return true;
376376
}
377377

378-
var otherSet = new SortedSet<T>(other, this.KeyComparer);
379-
if (this.Count != otherSet.Count)
378+
switch (other)
380379
{
381-
return false;
380+
case ImmutableSortedSet<T> otherAsImmutableSortedSet:
381+
if (EqualityComparer<IComparer<T>>.Default.Equals(this.KeyComparer, otherAsImmutableSortedSet.KeyComparer))
382+
{
383+
if (otherAsImmutableSortedSet.Count != this.Count)
384+
{
385+
return false;
386+
}
387+
return SetEqualsWithImmutableSortedSet(otherAsImmutableSortedSet, this);
388+
}
389+
390+
if (otherAsImmutableSortedSet.Count < this.Count)
391+
{
392+
return false;
393+
}
394+
break;
395+
396+
case SortedSet<T> otherAsSortedSet:
397+
if (EqualityComparer<IComparer<T>>.Default.Equals(this.KeyComparer, otherAsSortedSet.Comparer))
398+
{
399+
if (otherAsSortedSet.Count != this.Count)
400+
{
401+
return false;
402+
}
403+
return SetEqualsWithSortedSet(otherAsSortedSet, this);
404+
}
405+
406+
if (otherAsSortedSet.Count < this.Count)
407+
{
408+
return false;
409+
}
410+
break;
411+
412+
case ICollection<T> otherAsICollectionGeneric:
413+
// We check for < instead of != because other is not guaranteed to be a set; it could be a collection with duplicates.
414+
if (otherAsICollectionGeneric.Count < this.Count)
415+
{
416+
return false;
417+
}
418+
break;
382419
}
383420

384-
int matches = 0;
385-
foreach (T item in otherSet)
421+
var otherSet = new SortedSet<T>(other, this.KeyComparer);
422+
if (otherSet.Count != this.Count)
386423
{
387-
if (!this.Contains(item))
388-
{
389-
return false;
390-
}
391-
392-
matches++;
424+
return false;
393425
}
394426

395-
return matches == this.Count;
427+
return SetEqualsWithSortedSet(otherSet, this);
396428
}
397429

398430
/// <summary>
@@ -1079,6 +1111,42 @@ private ImmutableSortedSet<T> UnionIncremental(ReadOnlySpan<T> items)
10791111
return this.Wrap(result);
10801112
}
10811113

1114+
private static bool SetEqualsWithImmutableSortedSet(ImmutableSortedSet<T> other, ImmutableSortedSet<T> source)
1115+
{
1116+
// We can use a linear scan because both sets are sorted using the same comparer.
1117+
using var e = other.GetEnumerator();
1118+
foreach (T item in source)
1119+
{
1120+
bool eHasMore = e.MoveNext();
1121+
Debug.Assert(eHasMore);
1122+
1123+
if (source.KeyComparer.Compare(item, e.Current) != 0)
1124+
{
1125+
return false;
1126+
}
1127+
}
1128+
1129+
return true;
1130+
}
1131+
1132+
private static bool SetEqualsWithSortedSet(SortedSet<T> other, ImmutableSortedSet<T> source)
1133+
{
1134+
// We can use a linear scan because both sets are sorted using the same comparer.
1135+
using var e = other.GetEnumerator();
1136+
foreach (T item in source)
1137+
{
1138+
bool eHasMore = e.MoveNext();
1139+
Debug.Assert(eHasMore);
1140+
1141+
if (source.KeyComparer.Compare(item, e.Current) != 0)
1142+
{
1143+
return false;
1144+
}
1145+
}
1146+
1147+
return true;
1148+
}
1149+
10821150
/// <summary>
10831151
/// Creates a wrapping collection type around a root node.
10841152
/// </summary>

src/libraries/System.Collections.Immutable/tests/ImmutableSortedSetTest.cs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,96 @@ public void RandomOperationsTest()
7777
}
7878
}
7979

80+
[Fact]
81+
public void SetEqualsMismatchedComparersOriginInsensitiveOtherSensitiveReturnsTrue()
82+
{
83+
var ignoreCaseSet = ImmutableSortedSet.Create(StringComparer.OrdinalIgnoreCase, "a");
84+
var sensitiveSet = ImmutableSortedSet.Create(StringComparer.Ordinal, "a", "A");
85+
86+
Assert.True(ignoreCaseSet.SetEquals(sensitiveSet));
87+
}
88+
89+
[Fact]
90+
public void SetEqualsMismatchedComparersOriginSensitiveOtherInsensitiveReturnsFalse()
91+
{
92+
var sensitiveSetMain = ImmutableSortedSet.Create(StringComparer.Ordinal, "a");
93+
var insensitiveMutable = new SortedSet<string>(StringComparer.OrdinalIgnoreCase) { "a", "A" };
94+
95+
Assert.False(sensitiveSetMain.SetEquals(insensitiveMutable));
96+
}
97+
98+
[Fact]
99+
public void SetEqualsICollectionWithDuplicatesValidatesCorrectness()
100+
{
101+
var ignoreCaseSet = ImmutableSortedSet.Create(StringComparer.OrdinalIgnoreCase, "a");
102+
var listWithDupes = new List<string> { "a", "a", "a", "a" };
103+
104+
Assert.True(ignoreCaseSet.SetEquals(listWithDupes));
105+
}
106+
107+
[Fact]
108+
public void SetEqualsDifferentContentReturnsFalse()
109+
{
110+
var ignoreCaseSet = ImmutableSortedSet.Create(StringComparer.OrdinalIgnoreCase, "a");
111+
var setB = ImmutableSortedSet.Create(StringComparer.Ordinal, "b");
112+
113+
Assert.False(ignoreCaseSet.SetEquals(setB));
114+
}
115+
116+
[Fact]
117+
public void SetEqualsMismatchedComparersOtherCountSmallerReturnsFalse()
118+
{
119+
var originTwoElements = ImmutableSortedSet.Create(StringComparer.OrdinalIgnoreCase, "a", "b");
120+
var otherOneElement = ImmutableSortedSet.Create(StringComparer.Ordinal, "a");
121+
122+
Assert.False(originTwoElements.SetEquals(otherOneElement));
123+
}
124+
125+
[Fact]
126+
public void SetEqualsMatchedComparersDifferentCountsReturnsFalse()
127+
{
128+
var matchedSet1 = ImmutableSortedSet.Create(StringComparer.Ordinal, "1", "2");
129+
var matchedSet2 = ImmutableSortedSet.Create(StringComparer.Ordinal, "1");
130+
131+
Assert.False(matchedSet1.SetEquals(matchedSet2));
132+
}
133+
134+
[Fact]
135+
public void SetEqualsMatchedComparersSameContentReturnsTrue()
136+
{
137+
var matchedSet1 = ImmutableSortedSet.Create(StringComparer.Ordinal, "x", "y");
138+
var matchedSet2 = ImmutableSortedSet.Create(StringComparer.Ordinal, "y", "x");
139+
140+
Assert.True(matchedSet1.SetEquals(matchedSet2));
141+
}
142+
143+
[Fact]
144+
public void SetEqualsEmptySetsDifferentComparersReturnsTrue()
145+
{
146+
var empty1 = ImmutableSortedSet<string>.Empty.WithComparer(StringComparer.Ordinal);
147+
var empty2 = ImmutableSortedSet<string>.Empty.WithComparer(StringComparer.OrdinalIgnoreCase);
148+
149+
Assert.True(empty1.SetEquals(empty2));
150+
}
151+
152+
[Fact]
153+
public void SetEqualsMismatchedComparersOriginSensitiveOtherInsensitiveSameCountReturnsFalse()
154+
{
155+
var sensitiveSet = ImmutableSortedSet.Create(StringComparer.Ordinal, "a", "A");
156+
var insensitiveSet = ImmutableSortedSet.Create(StringComparer.OrdinalIgnoreCase, "a", "b");
157+
158+
Assert.False(sensitiveSet.SetEquals(insensitiveSet));
159+
}
160+
161+
[Fact]
162+
public void SetEqualsMismatchedComparersOtherIsLargerReturnsFalse()
163+
{
164+
var origin = ImmutableSortedSet.Create(StringComparer.OrdinalIgnoreCase, "a");
165+
var other = ImmutableSortedSet.Create(StringComparer.Ordinal, "a", "b");
166+
167+
Assert.False(origin.SetEquals(other));
168+
}
169+
80170
[Fact]
81171
public void CustomSort()
82172
{

0 commit comments

Comments
 (0)