@@ -387,6 +387,52 @@ internal readonly ref TValue GetValueRefOrNullRef(TKey key)
387387 goto Return ;
388388 }
389389
390+ private readonly int GetKeyIndex ( TKey key )
391+ {
392+ if ( key == null )
393+ {
394+ ThrowHelpers . ThrowArgumentNullException ( ThrowHelpers . Argument . key ) ;
395+ }
396+
397+ ref Entry entry = ref Polyfills . NullRef < Entry > ( ) ;
398+ if ( this . buckets != null )
399+ {
400+ Polyfills . DebugAssert ( this . entries != null , "expected entries to be != null" ) ;
401+
402+ var comparer = new DefaultEqualityComparer < TKey > ( ) ;
403+ uint hashCode = ( uint ) comparer . GetHashCode ( key ) ;
404+ int i = this . GetBucketRef ( hashCode ) ;
405+ var entries = this . entries ;
406+ uint collisionCount = 0 ;
407+ i -- ; // Value in _buckets is 1-based; subtract 1 from i. We do it here so it fuses with the following conditional.
408+ do
409+ {
410+ // Test in if to drop range check for following array access
411+ if ( ( uint ) i >= ( uint ) entries . Length )
412+ {
413+ return - 1 ;
414+ }
415+
416+ entry = ref entries [ i ] ;
417+ if ( entry . HashCode == hashCode && comparer . Equals ( entry . Key , key ) )
418+ {
419+ return i ;
420+ }
421+
422+ i = entry . Next ;
423+
424+ collisionCount ++ ;
425+ }
426+ while ( collisionCount <= ( uint ) entries . Length ) ;
427+
428+ // The chain of entries forms a loop; which means a concurrent update has happened.
429+ // Break out of the loop and throw, rather than looping forever.
430+ ThrowHelpers . ThrowInvalidOperationException_ConcurrentOperationsNotSupported ( ) ;
431+ }
432+
433+ return - 1 ;
434+ }
435+
390436 private int Initialize ( int minimumCapacity )
391437 {
392438 var capacity = HashHelpers . GetPrime ( minimumCapacity ) ;
@@ -842,22 +888,38 @@ internal readonly bool Keys_IsProperSubsetOf(IEnumerable<TKey> other)
842888 ThrowHelpers . ThrowArgumentNullException ( ThrowHelpers . Argument . other ) ;
843889 }
844890
845- var otherCopy = new RawSet < TKey > ( other ) ; // TODO: don't copy
846-
847- if ( this . Count >= otherCopy . Count )
891+ if ( Polyfills . TryGetNonEnumeratedCount ( other , out var otherCount ) )
848892 {
849- return false ;
893+ var count = this . Count ;
894+
895+ // No set is a proper subset of a set with less or equal number of elements.
896+ if ( otherCount <= count )
897+ {
898+ return false ;
899+ }
900+
901+ // The empty set is a proper subset of anything but the empty set.
902+ if ( count == 0 )
903+ {
904+ // Based on check above, other is not empty when Count == 0.
905+ return true ;
906+ }
850907 }
851908
852- foreach ( var entry in this )
909+ using var marker = new Marker ( in this ) ;
910+
911+ var otherHasAdditionalItems = false ;
912+
913+ // Note that enumerating `other` might trigger mutations on `this`.
914+ foreach ( var item in other )
853915 {
854- if ( ! otherCopy . Contains ( entry . Key ) )
916+ if ( ! marker . Mark ( item ) )
855917 {
856- return false ;
918+ otherHasAdditionalItems = true ;
857919 }
858920 }
859921
860- return true ;
922+ return marker . UnmarkedCount == 0 && otherHasAdditionalItems ;
861923 }
862924
863925 // Does not check/guard against concurrent mutation during enumeration!
@@ -868,24 +930,36 @@ internal readonly bool Keys_IsProperSupersetOf(IEnumerable<TKey> other)
868930 ThrowHelpers . ThrowArgumentNullException ( ThrowHelpers . Argument . other ) ;
869931 }
870932
871- if ( this . Count == 0 )
933+ var count = this . Count ;
934+
935+ // The empty set isn't a proper superset of any set.
936+ if ( count == 0 )
872937 {
873938 return false ;
874939 }
875940
876- var otherCopy = new RawSet < TKey > ( other ) ; // TODO: don't copy
941+ if ( Polyfills . TryGetNonEnumeratedCount ( other , out var otherCount ) )
942+ {
943+ // If other is the empty set then this is a superset.
944+ if ( otherCount == 0 )
945+ {
946+ // Note that this has at least one element, based on above check.
947+ return true ;
948+ }
949+ }
877950
878- int matchCount = 0 ;
879- foreach ( var item in otherCopy )
951+ using var marker = new Marker ( in this ) ;
952+
953+ // Note that enumerating `other` might trigger mutations on `this`.
954+ foreach ( var item in other )
880955 {
881- matchCount ++ ;
882- if ( ! this . ContainsKey ( item ) )
956+ if ( ! marker . Mark ( item ) )
883957 {
884958 return false ;
885959 }
886960 }
887961
888- return this . Count > matchCount ;
962+ return marker . UnmarkedCount > 0 ;
889963 }
890964
891965 // Does not check/guard against concurrent mutation during enumeration!
@@ -896,22 +970,31 @@ internal readonly bool Keys_IsSubsetOf(IEnumerable<TKey> other)
896970 ThrowHelpers . ThrowArgumentNullException ( ThrowHelpers . Argument . other ) ;
897971 }
898972
899- var otherCopy = new RawSet < TKey > ( other ) ; // TODO: don't copy
900-
901- if ( this . Count > otherCopy . Count )
973+ // The empty set is a subset of any set.
974+ var count = this . Count ;
975+ if ( count == 0 )
902976 {
903- return false ;
977+ return true ;
904978 }
905979
906- foreach ( var entry in this )
980+ if ( Polyfills . TryGetNonEnumeratedCount ( other , out var otherCount ) )
907981 {
908- if ( ! otherCopy . Contains ( entry . Key ) )
982+ // If this has more elements then it can't be a subset.
983+ if ( count > otherCount )
909984 {
910985 return false ;
911986 }
912987 }
913988
914- return true ;
989+ using var marker = new Marker ( in this ) ;
990+
991+ // Note that enumerating `other` might trigger mutations on `this`.
992+ foreach ( var item in other )
993+ {
994+ marker . Mark ( item ) ;
995+ }
996+
997+ return marker . UnmarkedCount == 0 ;
915998 }
916999
9171000 // Does not check/guard against concurrent mutation during enumeration!
@@ -965,22 +1048,35 @@ internal readonly bool Keys_SetEquals(IEnumerable<TKey> other)
9651048 ThrowHelpers . ThrowArgumentNullException ( ThrowHelpers . Argument . other ) ;
9661049 }
9671050
968- var otherCopy = new RawSet < TKey > ( other ) ; // TODO: don't copy
969-
970- if ( this . Count != otherCopy . Count )
1051+ if ( Polyfills . TryGetNonEnumeratedCount ( other , out var otherCount ) )
9711052 {
972- return false ;
1053+ var count = this . Count ;
1054+
1055+ // If this is empty, they are equal iff other is empty.
1056+ if ( count == 0 )
1057+ {
1058+ return otherCount == 0 ;
1059+ }
1060+
1061+ // Can't be equal if other set contains fewer elements than this.
1062+ if ( count > otherCount )
1063+ {
1064+ return false ;
1065+ }
9731066 }
9741067
975- foreach ( var item in otherCopy )
1068+ using var marker = new Marker ( in this ) ;
1069+
1070+ // Note that enumerating `other` might trigger mutations on `this`.
1071+ foreach ( var item in other )
9761072 {
977- if ( ! this . ContainsKey ( item ) )
1073+ if ( ! marker . Mark ( item ) )
9781074 {
9791075 return false ;
9801076 }
9811077 }
9821078
983- return true ;
1079+ return marker . UnmarkedCount == 0 ;
9841080 }
9851081
9861082 [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
@@ -1070,6 +1166,58 @@ public readonly override string ToString()
10701166 return builder . ToString ( ) ;
10711167 }
10721168
1169+ internal ref struct Marker
1170+ {
1171+ private readonly RawDictionary < TKey , TValue > dictionary ;
1172+ private readonly RawBitArray markings ;
1173+ private int unmarked ;
1174+
1175+ /// <summary>
1176+ /// How many elements in the set have not been marked yet.
1177+ /// </summary>
1178+ public readonly int UnmarkedCount
1179+ {
1180+ get
1181+ {
1182+ Polyfills . DebugAssert ( this . unmarked >= 0 ) ;
1183+
1184+ return this . unmarked ;
1185+ }
1186+ }
1187+
1188+ [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
1189+ public Marker ( ref readonly RawDictionary < TKey , TValue > set )
1190+ {
1191+ this . dictionary = set ;
1192+ this . markings = new RawBitArray ( set . end ) ;
1193+ this . unmarked = set . Count ;
1194+ }
1195+
1196+ // The RawSet may not be mutated in between calls to Mark.
1197+ public bool Mark ( TKey key )
1198+ {
1199+ int index = this . dictionary . GetKeyIndex ( key ) ;
1200+ if ( index < 0 )
1201+ {
1202+ return false ;
1203+ }
1204+
1205+ if ( ! this . markings [ index ] )
1206+ {
1207+ this . markings [ index ] = true ;
1208+ this . unmarked -- ;
1209+ }
1210+
1211+ return true ;
1212+ }
1213+
1214+ #pragma warning disable CA1822 // Mark members as static
1215+ public void Dispose ( )
1216+ {
1217+ }
1218+ #pragma warning restore CA1822 // Mark members as static
1219+ }
1220+
10731221 private readonly int GetArbitraryIndex ( )
10741222 {
10751223 // Use the hashcode of the backing `entries` array as a semi-random seed.
0 commit comments