From 001ae0fc192a94723be078d9276d1d8d3dab78ba Mon Sep 17 00:00:00 2001 From: aw0lid Date: Wed, 6 May 2026 17:30:50 +0300 Subject: [PATCH] Optimize ImmutableHashSet.IsSubsetOf to avoid unnecessary allocations --- .../Immutable/ImmutableHashSet_1.cs | 53 +++++++--- .../tests/ImmutableHashSetTest.cs | 100 ++++++++++++++++++ 2 files changed, 138 insertions(+), 15 deletions(-) diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Immutable/ImmutableHashSet_1.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Immutable/ImmutableHashSet_1.cs index 55f7e7e34f2ea6..1785a82676f17c 100644 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Immutable/ImmutableHashSet_1.cs +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Immutable/ImmutableHashSet_1.cs @@ -1010,25 +1010,48 @@ private static bool IsSubsetOf(IEnumerable other, MutationInput origin) return true; } - // To determine whether everything we have is also in another sequence, - // we enumerate the sequence and "tag" whether it's in this collection, - // then consider whether every element in this collection was tagged. - // Since this collection is immutable we cannot directly tag. So instead - // we simply count how many "hits" we have and ensure it's equal to the - // size of this collection. Of course for this to work we need to ensure - // the uniqueness of items in the given sequence, so we create a set based - // on the sequence first. + switch (other) + { + case ImmutableHashSet otherAsImmutableHashSet: + if (otherAsImmutableHashSet.Count < origin.Count) + { + return false; + } + + if (EqualityComparer>.Default.Equals(origin.EqualityComparer, otherAsImmutableHashSet.KeyComparer)) + { + return SetEqualsWithImmutableHashset(otherAsImmutableHashSet, origin); + } + break; + + case HashSet otherAsHashset: + if (otherAsHashset.Count < origin.Count) + { + return false; + } + + if (EqualityComparer>.Default.Equals(origin.EqualityComparer, otherAsHashset.Comparer)) + { + return SetEqualsWithHashset(otherAsHashset, origin); + } + break; + + case ICollection otherAsICollectionGeneric: + // We check for < instead of != because other is not guaranteed to be a set, it could be a collection with duplicates. + if (otherAsICollectionGeneric.Count < origin.Count) + { + return false; + } + break; + } + var otherSet = new HashSet(other, origin.EqualityComparer); - int matches = 0; - foreach (T item in otherSet) + if (otherSet.Count < origin.Count) { - if (Contains(item, origin)) - { - matches++; - } + return false; } - return matches == origin.Count; + return SetEqualsWithHashset(otherSet, origin); } #endregion diff --git a/src/libraries/System.Collections.Immutable/tests/ImmutableHashSetTest.cs b/src/libraries/System.Collections.Immutable/tests/ImmutableHashSetTest.cs index 12203d957186b7..3b656a397c746f 100644 --- a/src/libraries/System.Collections.Immutable/tests/ImmutableHashSetTest.cs +++ b/src/libraries/System.Collections.Immutable/tests/ImmutableHashSetTest.cs @@ -121,6 +121,106 @@ public void SetEqualsMismatchedComparersOtherIsLarger() Assert.False(origin.SetEquals(other)); } + [Fact] + public void IsSubsetOfMismatchedComparersLargerCountMissingElement() + { + var origin = ImmutableHashSet.Create(StringComparer.Ordinal, "a", "A"); + var other = ImmutableHashSet.Create(StringComparer.OrdinalIgnoreCase, "a", "b", "c", "d"); + + Assert.False(origin.IsSubsetOf(other)); + } + + [Fact] + public void IsSubsetOfICollectionLargeCountSmallUniqueSet() + { + var origin = ImmutableHashSet.Create(StringComparer.Ordinal, "a", "b"); + var other = new List { "a", "b", "b", "b" }; + + Assert.True(origin.IsSubsetOf(other)); + } + + [Fact] + public void IsSubsetOfEmptyAgainstAnything() + { + var empty = ImmutableHashSet.Empty; + var other = ImmutableHashSet.Create("any"); + + Assert.True(empty.IsSubsetOf(other)); + Assert.True(empty.IsSubsetOf(new List())); + } + + [Fact] + public void IsSubsetOfSensitiveOriginInsensitiveOther() + { + var origin = ImmutableHashSet.Create(StringComparer.Ordinal, "a"); + var other = ImmutableHashSet.Create(StringComparer.OrdinalIgnoreCase, "A", "b"); + + Assert.False(origin.IsSubsetOf(other)); + } + + [Fact] + public void IsSubsetOfMismatchedLogicallyEqual() + { + var origin = ImmutableHashSet.Create(StringComparer.OrdinalIgnoreCase, "a"); + var other = ImmutableHashSet.Create(StringComparer.Ordinal, "a", "A"); + + Assert.True(origin.IsSubsetOf(other)); + } + + [Fact] + public void IsSubsetOfInsensitiveOriginSensitiveOther() + { + var origin = ImmutableHashSet.Create(StringComparer.OrdinalIgnoreCase, "a", "b"); + var other = ImmutableHashSet.Create(StringComparer.Ordinal, "A", "B", "c"); + + Assert.True(origin.IsSubsetOf(other)); + } + + [Fact] + public void IsSubsetOfAgainstBCLHashSet() + { + var origin = ImmutableHashSet.Create(StringComparer.Ordinal, "a", "b"); + var other = new HashSet { "a", "b", "c" }; + + Assert.True(origin.IsSubsetOf(other)); + } + + [Fact] + public void IsSubsetOfListWithDuplicates() + { + var origin = ImmutableHashSet.Create(StringComparer.Ordinal, "a", "b", "c"); + var other = new List { "a", "b", "a", "b" }; + + Assert.False(origin.IsSubsetOf(other)); + } + + [Fact] + public void IsSubsetOfListWithDuplicatesAndCountSmaller() + { + var origin = ImmutableHashSet.Create("a", "b", "c"); + var other = new List { "a", "b" }; + + Assert.False(origin.IsSubsetOf(other)); + } + + [Fact] + public void IsSubsetOfOtherImmutableHashSetIsSmaller() + { + var origin = ImmutableHashSet.Create(StringComparer.Ordinal, "a", "b", "c"); + var other = ImmutableHashSet.Create(StringComparer.OrdinalIgnoreCase, "a", "b"); + + Assert.False(origin.IsSubsetOf(other)); + } + + [Fact] + public void IsSubsetOfOtherHashSetIsSmaller() + { + var origin = ImmutableHashSet.Create(StringComparer.Ordinal, "a", "b", "c"); + var other = new HashSet(StringComparer.OrdinalIgnoreCase) { "a", "b" }; + + Assert.False(origin.IsSubsetOf(other)); + } + [Fact] public void ChangeUnorderedEqualityComparer() {