Add FrozenDictionary/Set - #77799

Merged
stephentoub merged 3 commits into
dotnet:mainfrom
stephentoub:frozencollections
Nov 4, 2022
Merged

Add FrozenDictionary/Set#77799
stephentoub merged 3 commits into
dotnet:mainfrom
stephentoub:frozencollections

Conversation

@stephentoub

@stephentoubstephentoub commented Nov 2, 2022

Copy link
Copy Markdown
Member

Fixes#67209

The first commit imports @geeknoid's source, updated to compile in System.Collections.Immutable.dll.

The second commit overhauls the APIs and adds tests. Overhauling the APIs also required lots of surgery on various aspects of the implementation.

This includes a bunch of updates to our shared collections tests, which we've apparently not used in earnest with read-only collections, as a bunch of mutating tests were trying to run even when IsReadOnly was set to false.

There's still more work required here. Subsequent to this PR, we should:

  • Investigate ways to make construction faster.
  • Investigate ways to make lookups faster.
  • Investigate what exact special-cased implementations we want to ship.

(@geeknoid, your help here would be appreciated. Best case numbers look very good, e.g. the example benchmarks you previously created, but others don't look nearly as good. I vetted some of the worst numbers against the original implementation, and they're similar.)

Right now, for each of dictionary/set, there are custom implementations for:

  • Int32 keys with the default comparer
  • Value type keys with the default comparer
  • String keys with default/ordinal/ordinal-ignore-case comparers. Here there are two: one that tries to bucket a small number of strings by length, and one that tries to avoid reading/hashing the full strings by selecting the smallest portion that's unique enough.
  • A general fallback implementation that's used for everything else.

We can explore other data structures, other optimizations, etc., all behind the same veneer. Some of the existing implementations might not be worthwhile, too. For some situations, we might even want an implementation that just wraps Dictionary<,> along with dedicated keys/values arrays.

But the first step is getting the APIs merged to enable others to experiment.

Some benchmarks
[InProcess][GenericTypeArguments(typeof(string))][GenericTypeArguments(typeof(int))][GenericTypeArguments(typeof(object))][GenericTypeArguments(typeof(Guid))]publicclassDictionaryTests<TKey>{privateTKey[]_keys;privateTKey[]_nonExistentKeys;privateDictionary<TKey,long>_dictionary;privateFrozenDictionary<TKey,long>_frozenDictionary;[Params(1,16,1024)]publicintCount{get;set;}[Params(false,true)]publicboolDefaultComparer{get;set;}privatestaticIEqualityComparer<TKey>GetComparer(booldefaultComparer)=>defaultComparer?EqualityComparer<TKey>.Default:NonDefaultEqualityComparer<TKey>.Instance;privateunsafestaticTRandGetRandomValue<TRand>(Randomrand){if(typeof(TRand)==typeof(string)){Span<char>span=stackallocchar[rand.Next(5,20)];for(inti=0;i<span.Length;i++){span[i]=(char)('a'+rand.Next(0,26));}return(TRand)(object)span.ToString();}if(typeof(TRand)==typeof(object)){return(TRand)newobject();}if(typeof(TRand)==typeof(Guid)){Int128value=newInt128((ulong)rand.NextInt64(),(ulong)rand.NextInt64());return(TRand)(object)(*(Guid*)(&value));}if(typeof(TRand)==typeof(int)){return(TRand)(object)rand.Next();}if(typeof(TRand)==typeof(long)){return(TRand)(object)rand.NextInt64();}thrownewException("Unknown type");}[GlobalSetup]publicvoidSetup(){varrand=newRandom(42);_dictionary=newDictionary<TKey,long>(GetComparer(DefaultComparer));for(intj=0;j<Count;j++){_dictionary[GetRandomValue<TKey>(rand)]=GetRandomValue<long>(rand);}_frozenDictionary=_dictionary.ToFrozenDictionary(GetComparer(DefaultComparer));_keys=_dictionary.Keys.ToArray();if(typeof(TKey)==typeof(string)){_keys=(TKey[])(object)Array.ConvertAll((string[])(object)_keys,string.Copy);}_nonExistentKeys=newTKey[Count];for(inti=0;i<Count;i++){TKeykey;while(_dictionary.ContainsKey(key=GetRandomValue<TKey>(rand)));_nonExistentKeys[i]=key;}}[Benchmark]publicDictionary<TKey,long>Dictionary_Construct(){return_keys.ToDictionary((TKeyk)=>k,(TKeyk)=>0L,GetComparer(DefaultComparer));}[Benchmark]publicFrozenDictionary<TKey,long>FrozenDictionary_Construct(){return_keys.ToFrozenDictionary((TKeyk)=>k,(TKeyk)=>0L,GetComparer(DefaultComparer));}[Benchmark]publicboolDictionary_TryGetValue_Found(){boolallFound=true;TKey[]keys=_keys;foreach(TKeykeyinkeys){longvalue;allFound&=_dictionary.TryGetValue(key,outvalue);}returnallFound;}[Benchmark]publicboolDictionary_TryGetValue_NotFound(){boolanyFound=false;TKey[]nonExistentKeys=_nonExistentKeys;foreach(TKeykeyinnonExistentKeys){longvalue;anyFound|=_dictionary.TryGetValue(key,outvalue);}returnanyFound;}[Benchmark]publicboolFrozenDictionary_TryGetValue_Found(){boolallFound=true;TKey[]keys=_keys;foreach(TKeykeyinkeys){longvalue;allFound&=_frozenDictionary.TryGetValue(key,outvalue);}returnallFound;}[Benchmark]publicboolFrozenDictionary_TryGetValue_NotFound(){boolanyFound=false;TKey[]nonExistentKeys=_nonExistentKeys;foreach(TKeykeyinnonExistentKeys){longvalue;anyFound|=_frozenDictionary.TryGetValue(key,outvalue);}returnanyFound;}[Benchmark]publicintDictionary_Enumerate(){intcount=0;foreach(KeyValuePair<TKey,long>itemin_dictionary){_=item;count++;}returncount;}[Benchmark]publicintDictionary_EnumerateKeys(){intcount=0;foreach(TKeykeyin_dictionary.Keys){_=key;count++;}returncount;}[Benchmark]publiclongDictionary_EnumerateValues(){intcount=0;foreach(longvaluein_dictionary.Values){_=value;count++;}returncount;}[Benchmark]publicintFrozenDictionary_Enumerate(){intcount=0;foreach(KeyValuePair<TKey,long>itemin_frozenDictionary){_=item;count++;}returncount;}[Benchmark]publicintFrozenDictionary_EnumerateKeys(){intcount=0;foreach(TKeykeyin_frozenDictionary.Keys){count++;}returncount;}[Benchmark]publicintFrozenDictionary_EnumerateValues(){intcount=0;foreach(longvaluein_frozenDictionary.Values){count++;}returncount;}}internalsealedpartialclassNonDefaultEqualityComparer<T>:EqualityComparer<T>{publicstaticNonDefaultEqualityComparer<T>Instance{get;}=newNonDefaultEqualityComparer<T>();publicoverrideboolEquals(Tx,Ty)=>Default.Equals(x,y);publicoverrideintGetHashCode(Tobj)=>Default.GetHashCode(obj);}
TypeMethodCountDefaultComparerMean
DictionaryTests<Guid>Dictionary_Construct16False308.917 ns
DictionaryTests<Guid>FrozenDictionary_Construct16False2,546.966 ns
DictionaryTests<Int32>Dictionary_Construct16False256.919 ns
DictionaryTests<Int32>FrozenDictionary_Construct16False2,735.513 ns
DictionaryTests<Object>Dictionary_Construct16False459.599 ns
DictionaryTests<Object>FrozenDictionary_Construct16False2,990.372 ns
DictionaryTests<String>Dictionary_Construct16False574.025 ns
DictionaryTests<String>FrozenDictionary_Construct16False3,085.790 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found16False195.613 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found16False180.628 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found16False127.828 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found16False120.595 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found16False371.343 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found16False384.100 ns
DictionaryTests<String>Dictionary_TryGetValue_Found16False559.943 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found16False531.615 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound16False149.167 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound16False120.109 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound16False109.596 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound16False91.319 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound16False247.097 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound16False231.592 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound16False356.568 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound16False334.678 ns
DictionaryTests<Guid>Dictionary_Enumerate16False61.120 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate16False27.948 ns
DictionaryTests<Int32>Dictionary_Enumerate16False50.372 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate16False27.162 ns
DictionaryTests<Object>Dictionary_Enumerate16False89.827 ns
DictionaryTests<Object>FrozenDictionary_Enumerate16False74.392 ns
DictionaryTests<String>Dictionary_Enumerate16False89.446 ns
DictionaryTests<String>FrozenDictionary_Enumerate16False74.626 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys16False49.057 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys16False4.847 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys16False37.000 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys16False5.108 ns
DictionaryTests<Object>Dictionary_EnumerateKeys16False74.497 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys16False18.488 ns
DictionaryTests<String>Dictionary_EnumerateKeys16False74.906 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys16False18.571 ns
DictionaryTests<Guid>Dictionary_EnumerateValues16False38.145 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues16False5.054 ns
DictionaryTests<Int32>Dictionary_EnumerateValues16False36.834 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues16False5.103 ns
DictionaryTests<Object>Dictionary_EnumerateValues16False54.844 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues16False5.045 ns
DictionaryTests<String>Dictionary_EnumerateValues16False55.299 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues16False5.048 ns
DictionaryTests<Guid>Dictionary_Construct16True276.233 ns
DictionaryTests<Guid>FrozenDictionary_Construct16True2,480.023 ns
DictionaryTests<Int32>Dictionary_Construct16True247.043 ns
DictionaryTests<Int32>FrozenDictionary_Construct16True2,679.061 ns
DictionaryTests<Object>Dictionary_Construct16True401.734 ns
DictionaryTests<Object>FrozenDictionary_Construct16True2,782.085 ns
DictionaryTests<String>Dictionary_Construct16True360.930 ns
DictionaryTests<String>FrozenDictionary_Construct16True1,385.360 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found16True116.494 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found16True124.232 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found16True87.759 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found16True50.979 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found16True207.784 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found16True209.461 ns
DictionaryTests<String>Dictionary_TryGetValue_Found16True268.939 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found16True142.170 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound16True99.116 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound16True81.180 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound16True72.956 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound16True53.239 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound16True163.937 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound16True144.715 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound16True178.219 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound16True109.456 ns
DictionaryTests<Guid>Dictionary_Enumerate16True58.001 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate16True30.898 ns
DictionaryTests<Int32>Dictionary_Enumerate16True49.878 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate16True27.879 ns
DictionaryTests<Object>Dictionary_Enumerate16True89.847 ns
DictionaryTests<Object>FrozenDictionary_Enumerate16True74.568 ns
DictionaryTests<String>Dictionary_Enumerate16True89.347 ns
DictionaryTests<String>FrozenDictionary_Enumerate16True73.377 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys16True48.112 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys16True5.259 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys16True36.038 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys16True5.016 ns
DictionaryTests<Object>Dictionary_EnumerateKeys16True74.331 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys16True19.002 ns
DictionaryTests<String>Dictionary_EnumerateKeys16True73.520 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys16True18.961 ns
DictionaryTests<Guid>Dictionary_EnumerateValues16True39.389 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues16True5.432 ns
DictionaryTests<Int32>Dictionary_EnumerateValues16True37.072 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues16True4.999 ns
DictionaryTests<Object>Dictionary_EnumerateValues16True54.973 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues16True5.033 ns
DictionaryTests<String>Dictionary_EnumerateValues16True54.496 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues16True5.029 ns
DictionaryTests<Guid>Dictionary_Construct1024False20,251.116 ns
DictionaryTests<Guid>FrozenDictionary_Construct1024False165,973.254 ns
DictionaryTests<Int32>Dictionary_Construct1024False16,858.288 ns
DictionaryTests<Int32>FrozenDictionary_Construct1024False149,391.455 ns
DictionaryTests<Object>Dictionary_Construct1024False26,154.444 ns
DictionaryTests<Object>FrozenDictionary_Construct1024False174,545.846 ns
DictionaryTests<String>Dictionary_Construct1024False36,381.752 ns
DictionaryTests<String>FrozenDictionary_Construct1024False200,478.010 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found1024False16,232.339 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found1024False14,450.573 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found1024False10,903.514 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found1024False9,294.073 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found1024False24,834.107 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found1024False27,807.133 ns
DictionaryTests<String>Dictionary_TryGetValue_Found1024False43,093.660 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found1024False39,181.201 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound1024False13,773.527 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound1024False8,281.616 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound1024False10,252.792 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound1024False6,199.940 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound1024False20,427.130 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound1024False15,131.280 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound1024False26,598.257 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound1024False24,474.515 ns
DictionaryTests<Guid>Dictionary_Enumerate1024False3,440.145 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate1024False1,689.686 ns
DictionaryTests<Int32>Dictionary_Enumerate1024False2,520.821 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate1024False1,498.062 ns
DictionaryTests<Object>Dictionary_Enumerate1024False4,408.578 ns
DictionaryTests<Object>FrozenDictionary_Enumerate1024False3,689.598 ns
DictionaryTests<String>Dictionary_Enumerate1024False4,398.440 ns
DictionaryTests<String>FrozenDictionary_Enumerate1024False3,636.864 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys1024False2,359.947 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys1024False261.473 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys1024False2,033.189 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys1024False253.029 ns
DictionaryTests<Object>Dictionary_EnumerateKeys1024False3,742.122 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys1024False1,226.121 ns
DictionaryTests<String>Dictionary_EnumerateKeys1024False3,693.788 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys1024False1,222.912 ns
DictionaryTests<Guid>Dictionary_EnumerateValues1024False2,144.243 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues1024False260.725 ns
DictionaryTests<Int32>Dictionary_EnumerateValues1024False2,039.401 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues1024False255.137 ns
DictionaryTests<Object>Dictionary_EnumerateValues1024False2,900.210 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues1024False254.610 ns
DictionaryTests<String>Dictionary_EnumerateValues1024False2,924.998 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues1024False252.582 ns
DictionaryTests<Guid>Dictionary_Construct1024True17,653.020 ns
DictionaryTests<Guid>FrozenDictionary_Construct1024True165,900.270 ns
DictionaryTests<Int32>Dictionary_Construct1024True15,324.775 ns
DictionaryTests<Int32>FrozenDictionary_Construct1024True145,562.184 ns
DictionaryTests<Object>Dictionary_Construct1024True22,357.810 ns
DictionaryTests<Object>FrozenDictionary_Construct1024True164,976.669 ns
DictionaryTests<String>Dictionary_Construct1024True24,584.158 ns
DictionaryTests<String>FrozenDictionary_Construct1024True713,506.562 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found1024True10,972.369 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found1024True10,677.337 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found1024True6,061.961 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found1024True4,568.878 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found1024True15,521.277 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found1024True15,039.072 ns
DictionaryTests<String>Dictionary_TryGetValue_Found1024True21,252.949 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found1024True19,273.593 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound1024True10,827.871 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound1024True6,294.780 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound1024True8,168.398 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound1024True4,152.874 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound1024True14,748.903 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound1024True9,459.935 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound1024True20,727.839 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound1024True11,941.794 ns
DictionaryTests<Guid>Dictionary_Enumerate1024True3,355.945 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate1024True1,676.260 ns
DictionaryTests<Int32>Dictionary_Enumerate1024True2,501.281 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate1024True1,488.353 ns
DictionaryTests<Object>Dictionary_Enumerate1024True4,433.532 ns
DictionaryTests<Object>FrozenDictionary_Enumerate1024True3,662.125 ns
DictionaryTests<String>Dictionary_Enumerate1024True4,485.860 ns
DictionaryTests<String>FrozenDictionary_Enumerate1024True3,652.590 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys1024True2,351.717 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys1024True262.712 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys1024True2,019.313 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys1024True252.664 ns
DictionaryTests<Object>Dictionary_EnumerateKeys1024True3,673.043 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys1024True1,224.610 ns
DictionaryTests<String>Dictionary_EnumerateKeys1024True3,748.785 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys1024True1,228.282 ns
DictionaryTests<Guid>Dictionary_EnumerateValues1024True2,080.066 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues1024True255.457 ns
DictionaryTests<Int32>Dictionary_EnumerateValues1024True2,018.673 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues1024True255.488 ns
DictionaryTests<Object>Dictionary_EnumerateValues1024True2,872.265 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues1024True255.872 ns
DictionaryTests<String>Dictionary_EnumerateValues1024True2,895.610 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues1024True254.709 ns

@ghost

ghost commented Nov 2, 2022

Copy link
Copy Markdown

Note regarding the new-api-needs-documentation label:

This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, to please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change.

@ghost

ghost commented Nov 2, 2022

Copy link
Copy Markdown

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

Issue Details

The first commit imports @geeknoid's source, updated to compile in System.Collections.Immutable.dll.

The second commit overhauls the APIs and adds tests.

This includes a bunch of updates to our shared collections tests, which we've apparently not used in earnest with read-only collections, as a bunch of mutating tests were trying to run even when IsReadOnly was set to false.

There's still more work required here. Subsequent to this PR, we should:

  • Investigate ways to make construction faster.
  • Investigate ways to make lookups faster.
  • Investigate what exact special-cased implementations we want to ship.

(@geeknoid, your help here would be appreciated. Best case numbers look very good, e.g. the example benchmarks you previously created, but others don't look nearly as good. I vetted some of the worst numbers against the original implementation, and they're similar.)

Right now, for each of dictionary/set, there are custom implementations for:

  • Int32 keys with the default comparer
  • Value type keys with the default comparer
  • String keys with default/ordinal/ordinal-ignore-case comparers. Here there are two: one that tries to bucket a small number of strings by length, and one that tries to avoid reading/hashing the full strings by selecting the smallest portion that's unique enough.
  • A general fallback implementation that's used for everything else.

We can explore other data structures, other optimizations, etc., all behind the same veneer. Some of the existing implementations might not be worthwhile, too. For some situations, we might even want an implementation that just wraps Dictionary<,> along with dedicated keys/values arrays.

But the first step is getting the APIs merged to enable others to experiment.

Some benchmarks
[InProcess][GenericTypeArguments(typeof(string))][GenericTypeArguments(typeof(int))][GenericTypeArguments(typeof(object))][GenericTypeArguments(typeof(Guid))]publicclassDictionaryTests<TKey>{privateTKey[]_keys;privateTKey[]_nonExistentKeys;privateDictionary<TKey,long>_dictionary;privateFrozenDictionary<TKey,long>_frozenDictionary;[Params(1,16,1024)]publicintCount{get;set;}[Params(false,true)]publicboolDefaultComparer{get;set;}privatestaticIEqualityComparer<TKey>GetComparer(booldefaultComparer)=>defaultComparer?EqualityComparer<TKey>.Default:NonDefaultEqualityComparer<TKey>.Instance;privateunsafestaticTRandGetRandomValue<TRand>(Randomrand){if(typeof(TRand)==typeof(string)){Span<char>span=stackallocchar[rand.Next(5,20)];for(inti=0;i<span.Length;i++){span[i]=(char)('a'+rand.Next(0,26));}return(TRand)(object)span.ToString();}if(typeof(TRand)==typeof(object)){return(TRand)newobject();}if(typeof(TRand)==typeof(Guid)){Int128value=newInt128((ulong)rand.NextInt64(),(ulong)rand.NextInt64());return(TRand)(object)(*(Guid*)(&value));}if(typeof(TRand)==typeof(int)){return(TRand)(object)rand.Next();}if(typeof(TRand)==typeof(long)){return(TRand)(object)rand.NextInt64();}thrownewException("Unknown type");}[GlobalSetup]publicvoidSetup(){varrand=newRandom(42);_dictionary=newDictionary<TKey,long>(GetComparer(DefaultComparer));for(intj=0;j<Count;j++){_dictionary[GetRandomValue<TKey>(rand)]=GetRandomValue<long>(rand);}_frozenDictionary=_dictionary.ToFrozenDictionary(GetComparer(DefaultComparer));_keys=_dictionary.Keys.ToArray();if(typeof(TKey)==typeof(string)){_keys=(TKey[])(object)Array.ConvertAll((string[])(object)_keys,string.Copy);}_nonExistentKeys=newTKey[Count];for(inti=0;i<Count;i++){TKeykey;while(_dictionary.ContainsKey(key=GetRandomValue<TKey>(rand)));_nonExistentKeys[i]=key;}}[Benchmark]publicDictionary<TKey,long>Dictionary_Construct(){return_keys.ToDictionary((TKeyk)=>k,(TKeyk)=>0L,GetComparer(DefaultComparer));}[Benchmark]publicFrozenDictionary<TKey,long>FrozenDictionary_Construct(){return_keys.ToFrozenDictionary((TKeyk)=>k,(TKeyk)=>0L,GetComparer(DefaultComparer));}[Benchmark]publicboolDictionary_TryGetValue_Found(){boolallFound=true;TKey[]keys=_keys;foreach(TKeykeyinkeys){longvalue;allFound&=_dictionary.TryGetValue(key,outvalue);}returnallFound;}[Benchmark]publicboolDictionary_TryGetValue_NotFound(){boolanyFound=false;TKey[]nonExistentKeys=_nonExistentKeys;foreach(TKeykeyinnonExistentKeys){longvalue;anyFound|=_dictionary.TryGetValue(key,outvalue);}returnanyFound;}[Benchmark]publicboolFrozenDictionary_TryGetValue_Found(){boolallFound=true;TKey[]keys=_keys;foreach(TKeykeyinkeys){longvalue;allFound&=_frozenDictionary.TryGetValue(key,outvalue);}returnallFound;}[Benchmark]publicboolFrozenDictionary_TryGetValue_NotFound(){boolanyFound=false;TKey[]nonExistentKeys=_nonExistentKeys;foreach(TKeykeyinnonExistentKeys){longvalue;anyFound|=_frozenDictionary.TryGetValue(key,outvalue);}returnanyFound;}[Benchmark]publicintDictionary_Enumerate(){intcount=0;foreach(KeyValuePair<TKey,long>itemin_dictionary){_=item;count++;}returncount;}[Benchmark]publicintDictionary_EnumerateKeys(){intcount=0;foreach(TKeykeyin_dictionary.Keys){_=key;count++;}returncount;}[Benchmark]publiclongDictionary_EnumerateValues(){intcount=0;foreach(longvaluein_dictionary.Values){_=value;count++;}returncount;}[Benchmark]publicintFrozenDictionary_Enumerate(){intcount=0;foreach(KeyValuePair<TKey,long>itemin_frozenDictionary){_=item;count++;}returncount;}[Benchmark]publicintFrozenDictionary_EnumerateKeys(){intcount=0;foreach(TKeykeyin_frozenDictionary.Keys){count++;}returncount;}[Benchmark]publicintFrozenDictionary_EnumerateValues(){intcount=0;foreach(longvaluein_frozenDictionary.Values){count++;}returncount;}}internalsealedpartialclassNonDefaultEqualityComparer<T>:EqualityComparer<T>{publicstaticNonDefaultEqualityComparer<T>Instance{get;}=newNonDefaultEqualityComparer<T>();publicoverrideboolEquals(Tx,Ty)=>Default.Equals(x,y);publicoverrideintGetHashCode(Tobj)=>Default.GetHashCode(obj);}
TypeMethodCountDefaultComparerMean
DictionaryTests<Guid>Dictionary_Construct16False308.917 ns
DictionaryTests<Guid>FrozenDictionary_Construct16False2,546.966 ns
DictionaryTests<Int32>Dictionary_Construct16False256.919 ns
DictionaryTests<Int32>FrozenDictionary_Construct16False2,735.513 ns
DictionaryTests<Object>Dictionary_Construct16False459.599 ns
DictionaryTests<Object>FrozenDictionary_Construct16False2,990.372 ns
DictionaryTests<String>Dictionary_Construct16False574.025 ns
DictionaryTests<String>FrozenDictionary_Construct16False3,085.790 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found16False195.613 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found16False180.628 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found16False127.828 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found16False120.595 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found16False371.343 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found16False384.100 ns
DictionaryTests<String>Dictionary_TryGetValue_Found16False559.943 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found16False531.615 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound16False149.167 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound16False120.109 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound16False109.596 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound16False91.319 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound16False247.097 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound16False231.592 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound16False356.568 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound16False334.678 ns
DictionaryTests<Guid>Dictionary_Enumerate16False61.120 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate16False27.948 ns
DictionaryTests<Int32>Dictionary_Enumerate16False50.372 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate16False27.162 ns
DictionaryTests<Object>Dictionary_Enumerate16False89.827 ns
DictionaryTests<Object>FrozenDictionary_Enumerate16False74.392 ns
DictionaryTests<String>Dictionary_Enumerate16False89.446 ns
DictionaryTests<String>FrozenDictionary_Enumerate16False74.626 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys16False49.057 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys16False4.847 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys16False37.000 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys16False5.108 ns
DictionaryTests<Object>Dictionary_EnumerateKeys16False74.497 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys16False18.488 ns
DictionaryTests<String>Dictionary_EnumerateKeys16False74.906 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys16False18.571 ns
DictionaryTests<Guid>Dictionary_EnumerateValues16False38.145 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues16False5.054 ns
DictionaryTests<Int32>Dictionary_EnumerateValues16False36.834 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues16False5.103 ns
DictionaryTests<Object>Dictionary_EnumerateValues16False54.844 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues16False5.045 ns
DictionaryTests<String>Dictionary_EnumerateValues16False55.299 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues16False5.048 ns
DictionaryTests<Guid>Dictionary_Construct16True276.233 ns
DictionaryTests<Guid>FrozenDictionary_Construct16True2,480.023 ns
DictionaryTests<Int32>Dictionary_Construct16True247.043 ns
DictionaryTests<Int32>FrozenDictionary_Construct16True2,679.061 ns
DictionaryTests<Object>Dictionary_Construct16True401.734 ns
DictionaryTests<Object>FrozenDictionary_Construct16True2,782.085 ns
DictionaryTests<String>Dictionary_Construct16True360.930 ns
DictionaryTests<String>FrozenDictionary_Construct16True1,385.360 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found16True116.494 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found16True124.232 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found16True87.759 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found16True50.979 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found16True207.784 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found16True209.461 ns
DictionaryTests<String>Dictionary_TryGetValue_Found16True268.939 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found16True142.170 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound16True99.116 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound16True81.180 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound16True72.956 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound16True53.239 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound16True163.937 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound16True144.715 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound16True178.219 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound16True109.456 ns
DictionaryTests<Guid>Dictionary_Enumerate16True58.001 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate16True30.898 ns
DictionaryTests<Int32>Dictionary_Enumerate16True49.878 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate16True27.879 ns
DictionaryTests<Object>Dictionary_Enumerate16True89.847 ns
DictionaryTests<Object>FrozenDictionary_Enumerate16True74.568 ns
DictionaryTests<String>Dictionary_Enumerate16True89.347 ns
DictionaryTests<String>FrozenDictionary_Enumerate16True73.377 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys16True48.112 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys16True5.259 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys16True36.038 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys16True5.016 ns
DictionaryTests<Object>Dictionary_EnumerateKeys16True74.331 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys16True19.002 ns
DictionaryTests<String>Dictionary_EnumerateKeys16True73.520 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys16True18.961 ns
DictionaryTests<Guid>Dictionary_EnumerateValues16True39.389 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues16True5.432 ns
DictionaryTests<Int32>Dictionary_EnumerateValues16True37.072 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues16True4.999 ns
DictionaryTests<Object>Dictionary_EnumerateValues16True54.973 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues16True5.033 ns
DictionaryTests<String>Dictionary_EnumerateValues16True54.496 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues16True5.029 ns
DictionaryTests<Guid>Dictionary_Construct1024False20,251.116 ns
DictionaryTests<Guid>FrozenDictionary_Construct1024False165,973.254 ns
DictionaryTests<Int32>Dictionary_Construct1024False16,858.288 ns
DictionaryTests<Int32>FrozenDictionary_Construct1024False149,391.455 ns
DictionaryTests<Object>Dictionary_Construct1024False26,154.444 ns
DictionaryTests<Object>FrozenDictionary_Construct1024False174,545.846 ns
DictionaryTests<String>Dictionary_Construct1024False36,381.752 ns
DictionaryTests<String>FrozenDictionary_Construct1024False200,478.010 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found1024False16,232.339 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found1024False14,450.573 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found1024False10,903.514 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found1024False9,294.073 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found1024False24,834.107 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found1024False27,807.133 ns
DictionaryTests<String>Dictionary_TryGetValue_Found1024False43,093.660 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found1024False39,181.201 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound1024False13,773.527 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound1024False8,281.616 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound1024False10,252.792 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound1024False6,199.940 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound1024False20,427.130 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound1024False15,131.280 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound1024False26,598.257 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound1024False24,474.515 ns
DictionaryTests<Guid>Dictionary_Enumerate1024False3,440.145 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate1024False1,689.686 ns
DictionaryTests<Int32>Dictionary_Enumerate1024False2,520.821 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate1024False1,498.062 ns
DictionaryTests<Object>Dictionary_Enumerate1024False4,408.578 ns
DictionaryTests<Object>FrozenDictionary_Enumerate1024False3,689.598 ns
DictionaryTests<String>Dictionary_Enumerate1024False4,398.440 ns
DictionaryTests<String>FrozenDictionary_Enumerate1024False3,636.864 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys1024False2,359.947 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys1024False261.473 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys1024False2,033.189 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys1024False253.029 ns
DictionaryTests<Object>Dictionary_EnumerateKeys1024False3,742.122 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys1024False1,226.121 ns
DictionaryTests<String>Dictionary_EnumerateKeys1024False3,693.788 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys1024False1,222.912 ns
DictionaryTests<Guid>Dictionary_EnumerateValues1024False2,144.243 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues1024False260.725 ns
DictionaryTests<Int32>Dictionary_EnumerateValues1024False2,039.401 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues1024False255.137 ns
DictionaryTests<Object>Dictionary_EnumerateValues1024False2,900.210 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues1024False254.610 ns
DictionaryTests<String>Dictionary_EnumerateValues1024False2,924.998 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues1024False252.582 ns
DictionaryTests<Guid>Dictionary_Construct1024True17,653.020 ns
DictionaryTests<Guid>FrozenDictionary_Construct1024True165,900.270 ns
DictionaryTests<Int32>Dictionary_Construct1024True15,324.775 ns
DictionaryTests<Int32>FrozenDictionary_Construct1024True145,562.184 ns
DictionaryTests<Object>Dictionary_Construct1024True22,357.810 ns
DictionaryTests<Object>FrozenDictionary_Construct1024True164,976.669 ns
DictionaryTests<String>Dictionary_Construct1024True24,584.158 ns
DictionaryTests<String>FrozenDictionary_Construct1024True713,506.562 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found1024True10,972.369 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found1024True10,677.337 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found1024True6,061.961 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found1024True4,568.878 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found1024True15,521.277 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found1024True15,039.072 ns
DictionaryTests<String>Dictionary_TryGetValue_Found1024True21,252.949 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found1024True19,273.593 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound1024True10,827.871 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound1024True6,294.780 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound1024True8,168.398 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound1024True4,152.874 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound1024True14,748.903 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound1024True9,459.935 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound1024True20,727.839 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound1024True11,941.794 ns
DictionaryTests<Guid>Dictionary_Enumerate1024True3,355.945 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate1024True1,676.260 ns
DictionaryTests<Int32>Dictionary_Enumerate1024True2,501.281 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate1024True1,488.353 ns
DictionaryTests<Object>Dictionary_Enumerate1024True4,433.532 ns
DictionaryTests<Object>FrozenDictionary_Enumerate1024True3,662.125 ns
DictionaryTests<String>Dictionary_Enumerate1024True4,485.860 ns
DictionaryTests<String>FrozenDictionary_Enumerate1024True3,652.590 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys1024True2,351.717 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys1024True262.712 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys1024True2,019.313 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys1024True252.664 ns
DictionaryTests<Object>Dictionary_EnumerateKeys1024True3,673.043 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys1024True1,224.610 ns
DictionaryTests<String>Dictionary_EnumerateKeys1024True3,748.785 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys1024True1,228.282 ns
DictionaryTests<Guid>Dictionary_EnumerateValues1024True2,080.066 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues1024True255.457 ns
DictionaryTests<Int32>Dictionary_EnumerateValues1024True2,018.673 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues1024True255.488 ns
DictionaryTests<Object>Dictionary_EnumerateValues1024True2,872.265 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues1024True255.872 ns
DictionaryTests<String>Dictionary_EnumerateValues1024True2,895.610 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues1024True254.709 ns
Author:stephentoub
Assignees:-
Labels:

area-System.Collections

Milestone:8.0.0

@ghostghost assigned stephentoubNov 2, 2022
@geeknoid

Copy link
Copy Markdown
Member

The read benchmarks look pretty good, but the construction phase is definitely on the slow side. I never really measure the construction time since for my scenarios I didn't care so much. I do think adding some notion of "intensity" in the constructor to let the caller decide how much time to spend optimizing would be desirable.

It would be interesting in particular to see what the read perf is if the implementation doesn't perform any optimization at all. It would likely still be faster than a normal Dictionary/HashSet when using the default comparers, but not by much. But if that case is still a bit faster and construction time is on par with normal Dictionary/HashSet, then that says that any scenario that needs a readonly dictionary or set should be using the frozen ones by default since it would be faster.

Without the ability to tune how much time is spent constructing, then you have to be careful where you use the frozen collections and make sure the cost/benefit will be worth it.

@geeknoid

geeknoid commented Nov 2, 2022

Copy link
Copy Markdown
Member

Some thoughts for the future:

  • Dictionaries and sets with low cardinality are common. We should look at adding specialized implementation for 1 and 2 entry collections. This saves memory and speeds things up.

  • I had on my todo list to create specialized two character string comparers to optimize that case. For small collections, often the single char comparer is enough and works well. But when the collection grows, you run out of single characters to compare, so the code falls back to two.

  • We discussed in the review that the majority use case for this kind of collection is where the data comes dynamically into the process. This says that doing the optimization work for the collections in a source generator is not likely to be used frequently. However, what I think might be useful is the ability to serialize/deserialize the internal state for the frozen collections in a portable way. This would let me distribute this serialized state as config, and thus forego the construction overhead. So instead of putting a bunch of key/value pairs in my config and having to optimize those in each of my service's processes, I could optimize the key/value pairs offline and push the resulting state to the processes.

stephentouband others added 2 commits November 2, 2022 17:45
…y compile
Co-authored-by: Martin Taillefer <mataille@microsoft.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>

@geeknoidgeeknoid left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This looks great.

@krwq

krwq commented Nov 4, 2022

Copy link
Copy Markdown
Member

I'd be really interested to see how integration of this feature would look like with System.Text.Json (both JsonSerializerOptions.Converters and JsonTypeInfo.Properties are freezing values at one point)

krwq
krwq approved these changes Nov 4, 2022

@krwqkrwq left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I've skimmed all of the files and overall looks good. I'm really impressed in the amount of heuristics done here to make this super optimized. Thank you!

@stephentoub
stephentoub merged commit 6c5a440 into dotnet:mainNov 4, 2022
@stephentoub
stephentoub deleted the frozencollections branch November 4, 2022 19:49
@ghostghost locked as resolved and limited conversation to collaborators Dec 5, 2022
@jeffhandleyjeffhandley added the blog-candidate Completed PRs that are candidate topics for blog post coverage label Mar 14, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Collectionsblog-candidateCompleted PRs that are candidate topics for blog post coveragenew-api-needs-documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[API Proposal]: Provide optimized read-only collections

4 participants

@stephentoub@geeknoid@krwq@jeffhandley
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Add FrozenDictionary/Set - #77799

Merged
stephentoub merged 3 commits into
dotnet:mainfrom
stephentoub:frozencollections
Nov 4, 2022
Merged

Add FrozenDictionary/Set#77799
stephentoub merged 3 commits into
dotnet:mainfrom
stephentoub:frozencollections

Conversation

@stephentoub

@stephentoubstephentoub commented Nov 2, 2022

Copy link
Copy Markdown
Member

Fixes#67209

The first commit imports @geeknoid's source, updated to compile in System.Collections.Immutable.dll.

The second commit overhauls the APIs and adds tests. Overhauling the APIs also required lots of surgery on various aspects of the implementation.

This includes a bunch of updates to our shared collections tests, which we've apparently not used in earnest with read-only collections, as a bunch of mutating tests were trying to run even when IsReadOnly was set to false.

There's still more work required here. Subsequent to this PR, we should:

  • Investigate ways to make construction faster.
  • Investigate ways to make lookups faster.
  • Investigate what exact special-cased implementations we want to ship.

(@geeknoid, your help here would be appreciated. Best case numbers look very good, e.g. the example benchmarks you previously created, but others don't look nearly as good. I vetted some of the worst numbers against the original implementation, and they're similar.)

Right now, for each of dictionary/set, there are custom implementations for:

  • Int32 keys with the default comparer
  • Value type keys with the default comparer
  • String keys with default/ordinal/ordinal-ignore-case comparers. Here there are two: one that tries to bucket a small number of strings by length, and one that tries to avoid reading/hashing the full strings by selecting the smallest portion that's unique enough.
  • A general fallback implementation that's used for everything else.

We can explore other data structures, other optimizations, etc., all behind the same veneer. Some of the existing implementations might not be worthwhile, too. For some situations, we might even want an implementation that just wraps Dictionary<,> along with dedicated keys/values arrays.

But the first step is getting the APIs merged to enable others to experiment.

Some benchmarks
[InProcess][GenericTypeArguments(typeof(string))][GenericTypeArguments(typeof(int))][GenericTypeArguments(typeof(object))][GenericTypeArguments(typeof(Guid))]publicclassDictionaryTests<TKey>{privateTKey[]_keys;privateTKey[]_nonExistentKeys;privateDictionary<TKey,long>_dictionary;privateFrozenDictionary<TKey,long>_frozenDictionary;[Params(1,16,1024)]publicintCount{get;set;}[Params(false,true)]publicboolDefaultComparer{get;set;}privatestaticIEqualityComparer<TKey>GetComparer(booldefaultComparer)=>defaultComparer?EqualityComparer<TKey>.Default:NonDefaultEqualityComparer<TKey>.Instance;privateunsafestaticTRandGetRandomValue<TRand>(Randomrand){if(typeof(TRand)==typeof(string)){Span<char>span=stackallocchar[rand.Next(5,20)];for(inti=0;i<span.Length;i++){span[i]=(char)('a'+rand.Next(0,26));}return(TRand)(object)span.ToString();}if(typeof(TRand)==typeof(object)){return(TRand)newobject();}if(typeof(TRand)==typeof(Guid)){Int128value=newInt128((ulong)rand.NextInt64(),(ulong)rand.NextInt64());return(TRand)(object)(*(Guid*)(&value));}if(typeof(TRand)==typeof(int)){return(TRand)(object)rand.Next();}if(typeof(TRand)==typeof(long)){return(TRand)(object)rand.NextInt64();}thrownewException("Unknown type");}[GlobalSetup]publicvoidSetup(){varrand=newRandom(42);_dictionary=newDictionary<TKey,long>(GetComparer(DefaultComparer));for(intj=0;j<Count;j++){_dictionary[GetRandomValue<TKey>(rand)]=GetRandomValue<long>(rand);}_frozenDictionary=_dictionary.ToFrozenDictionary(GetComparer(DefaultComparer));_keys=_dictionary.Keys.ToArray();if(typeof(TKey)==typeof(string)){_keys=(TKey[])(object)Array.ConvertAll((string[])(object)_keys,string.Copy);}_nonExistentKeys=newTKey[Count];for(inti=0;i<Count;i++){TKeykey;while(_dictionary.ContainsKey(key=GetRandomValue<TKey>(rand)));_nonExistentKeys[i]=key;}}[Benchmark]publicDictionary<TKey,long>Dictionary_Construct(){return_keys.ToDictionary((TKeyk)=>k,(TKeyk)=>0L,GetComparer(DefaultComparer));}[Benchmark]publicFrozenDictionary<TKey,long>FrozenDictionary_Construct(){return_keys.ToFrozenDictionary((TKeyk)=>k,(TKeyk)=>0L,GetComparer(DefaultComparer));}[Benchmark]publicboolDictionary_TryGetValue_Found(){boolallFound=true;TKey[]keys=_keys;foreach(TKeykeyinkeys){longvalue;allFound&=_dictionary.TryGetValue(key,outvalue);}returnallFound;}[Benchmark]publicboolDictionary_TryGetValue_NotFound(){boolanyFound=false;TKey[]nonExistentKeys=_nonExistentKeys;foreach(TKeykeyinnonExistentKeys){longvalue;anyFound|=_dictionary.TryGetValue(key,outvalue);}returnanyFound;}[Benchmark]publicboolFrozenDictionary_TryGetValue_Found(){boolallFound=true;TKey[]keys=_keys;foreach(TKeykeyinkeys){longvalue;allFound&=_frozenDictionary.TryGetValue(key,outvalue);}returnallFound;}[Benchmark]publicboolFrozenDictionary_TryGetValue_NotFound(){boolanyFound=false;TKey[]nonExistentKeys=_nonExistentKeys;foreach(TKeykeyinnonExistentKeys){longvalue;anyFound|=_frozenDictionary.TryGetValue(key,outvalue);}returnanyFound;}[Benchmark]publicintDictionary_Enumerate(){intcount=0;foreach(KeyValuePair<TKey,long>itemin_dictionary){_=item;count++;}returncount;}[Benchmark]publicintDictionary_EnumerateKeys(){intcount=0;foreach(TKeykeyin_dictionary.Keys){_=key;count++;}returncount;}[Benchmark]publiclongDictionary_EnumerateValues(){intcount=0;foreach(longvaluein_dictionary.Values){_=value;count++;}returncount;}[Benchmark]publicintFrozenDictionary_Enumerate(){intcount=0;foreach(KeyValuePair<TKey,long>itemin_frozenDictionary){_=item;count++;}returncount;}[Benchmark]publicintFrozenDictionary_EnumerateKeys(){intcount=0;foreach(TKeykeyin_frozenDictionary.Keys){count++;}returncount;}[Benchmark]publicintFrozenDictionary_EnumerateValues(){intcount=0;foreach(longvaluein_frozenDictionary.Values){count++;}returncount;}}internalsealedpartialclassNonDefaultEqualityComparer<T>:EqualityComparer<T>{publicstaticNonDefaultEqualityComparer<T>Instance{get;}=newNonDefaultEqualityComparer<T>();publicoverrideboolEquals(Tx,Ty)=>Default.Equals(x,y);publicoverrideintGetHashCode(Tobj)=>Default.GetHashCode(obj);}
TypeMethodCountDefaultComparerMean
DictionaryTests<Guid>Dictionary_Construct16False308.917 ns
DictionaryTests<Guid>FrozenDictionary_Construct16False2,546.966 ns
DictionaryTests<Int32>Dictionary_Construct16False256.919 ns
DictionaryTests<Int32>FrozenDictionary_Construct16False2,735.513 ns
DictionaryTests<Object>Dictionary_Construct16False459.599 ns
DictionaryTests<Object>FrozenDictionary_Construct16False2,990.372 ns
DictionaryTests<String>Dictionary_Construct16False574.025 ns
DictionaryTests<String>FrozenDictionary_Construct16False3,085.790 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found16False195.613 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found16False180.628 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found16False127.828 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found16False120.595 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found16False371.343 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found16False384.100 ns
DictionaryTests<String>Dictionary_TryGetValue_Found16False559.943 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found16False531.615 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound16False149.167 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound16False120.109 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound16False109.596 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound16False91.319 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound16False247.097 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound16False231.592 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound16False356.568 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound16False334.678 ns
DictionaryTests<Guid>Dictionary_Enumerate16False61.120 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate16False27.948 ns
DictionaryTests<Int32>Dictionary_Enumerate16False50.372 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate16False27.162 ns
DictionaryTests<Object>Dictionary_Enumerate16False89.827 ns
DictionaryTests<Object>FrozenDictionary_Enumerate16False74.392 ns
DictionaryTests<String>Dictionary_Enumerate16False89.446 ns
DictionaryTests<String>FrozenDictionary_Enumerate16False74.626 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys16False49.057 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys16False4.847 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys16False37.000 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys16False5.108 ns
DictionaryTests<Object>Dictionary_EnumerateKeys16False74.497 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys16False18.488 ns
DictionaryTests<String>Dictionary_EnumerateKeys16False74.906 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys16False18.571 ns
DictionaryTests<Guid>Dictionary_EnumerateValues16False38.145 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues16False5.054 ns
DictionaryTests<Int32>Dictionary_EnumerateValues16False36.834 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues16False5.103 ns
DictionaryTests<Object>Dictionary_EnumerateValues16False54.844 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues16False5.045 ns
DictionaryTests<String>Dictionary_EnumerateValues16False55.299 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues16False5.048 ns
DictionaryTests<Guid>Dictionary_Construct16True276.233 ns
DictionaryTests<Guid>FrozenDictionary_Construct16True2,480.023 ns
DictionaryTests<Int32>Dictionary_Construct16True247.043 ns
DictionaryTests<Int32>FrozenDictionary_Construct16True2,679.061 ns
DictionaryTests<Object>Dictionary_Construct16True401.734 ns
DictionaryTests<Object>FrozenDictionary_Construct16True2,782.085 ns
DictionaryTests<String>Dictionary_Construct16True360.930 ns
DictionaryTests<String>FrozenDictionary_Construct16True1,385.360 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found16True116.494 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found16True124.232 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found16True87.759 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found16True50.979 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found16True207.784 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found16True209.461 ns
DictionaryTests<String>Dictionary_TryGetValue_Found16True268.939 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found16True142.170 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound16True99.116 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound16True81.180 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound16True72.956 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound16True53.239 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound16True163.937 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound16True144.715 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound16True178.219 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound16True109.456 ns
DictionaryTests<Guid>Dictionary_Enumerate16True58.001 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate16True30.898 ns
DictionaryTests<Int32>Dictionary_Enumerate16True49.878 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate16True27.879 ns
DictionaryTests<Object>Dictionary_Enumerate16True89.847 ns
DictionaryTests<Object>FrozenDictionary_Enumerate16True74.568 ns
DictionaryTests<String>Dictionary_Enumerate16True89.347 ns
DictionaryTests<String>FrozenDictionary_Enumerate16True73.377 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys16True48.112 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys16True5.259 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys16True36.038 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys16True5.016 ns
DictionaryTests<Object>Dictionary_EnumerateKeys16True74.331 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys16True19.002 ns
DictionaryTests<String>Dictionary_EnumerateKeys16True73.520 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys16True18.961 ns
DictionaryTests<Guid>Dictionary_EnumerateValues16True39.389 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues16True5.432 ns
DictionaryTests<Int32>Dictionary_EnumerateValues16True37.072 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues16True4.999 ns
DictionaryTests<Object>Dictionary_EnumerateValues16True54.973 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues16True5.033 ns
DictionaryTests<String>Dictionary_EnumerateValues16True54.496 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues16True5.029 ns
DictionaryTests<Guid>Dictionary_Construct1024False20,251.116 ns
DictionaryTests<Guid>FrozenDictionary_Construct1024False165,973.254 ns
DictionaryTests<Int32>Dictionary_Construct1024False16,858.288 ns
DictionaryTests<Int32>FrozenDictionary_Construct1024False149,391.455 ns
DictionaryTests<Object>Dictionary_Construct1024False26,154.444 ns
DictionaryTests<Object>FrozenDictionary_Construct1024False174,545.846 ns
DictionaryTests<String>Dictionary_Construct1024False36,381.752 ns
DictionaryTests<String>FrozenDictionary_Construct1024False200,478.010 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found1024False16,232.339 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found1024False14,450.573 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found1024False10,903.514 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found1024False9,294.073 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found1024False24,834.107 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found1024False27,807.133 ns
DictionaryTests<String>Dictionary_TryGetValue_Found1024False43,093.660 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found1024False39,181.201 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound1024False13,773.527 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound1024False8,281.616 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound1024False10,252.792 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound1024False6,199.940 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound1024False20,427.130 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound1024False15,131.280 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound1024False26,598.257 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound1024False24,474.515 ns
DictionaryTests<Guid>Dictionary_Enumerate1024False3,440.145 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate1024False1,689.686 ns
DictionaryTests<Int32>Dictionary_Enumerate1024False2,520.821 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate1024False1,498.062 ns
DictionaryTests<Object>Dictionary_Enumerate1024False4,408.578 ns
DictionaryTests<Object>FrozenDictionary_Enumerate1024False3,689.598 ns
DictionaryTests<String>Dictionary_Enumerate1024False4,398.440 ns
DictionaryTests<String>FrozenDictionary_Enumerate1024False3,636.864 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys1024False2,359.947 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys1024False261.473 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys1024False2,033.189 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys1024False253.029 ns
DictionaryTests<Object>Dictionary_EnumerateKeys1024False3,742.122 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys1024False1,226.121 ns
DictionaryTests<String>Dictionary_EnumerateKeys1024False3,693.788 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys1024False1,222.912 ns
DictionaryTests<Guid>Dictionary_EnumerateValues1024False2,144.243 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues1024False260.725 ns
DictionaryTests<Int32>Dictionary_EnumerateValues1024False2,039.401 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues1024False255.137 ns
DictionaryTests<Object>Dictionary_EnumerateValues1024False2,900.210 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues1024False254.610 ns
DictionaryTests<String>Dictionary_EnumerateValues1024False2,924.998 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues1024False252.582 ns
DictionaryTests<Guid>Dictionary_Construct1024True17,653.020 ns
DictionaryTests<Guid>FrozenDictionary_Construct1024True165,900.270 ns
DictionaryTests<Int32>Dictionary_Construct1024True15,324.775 ns
DictionaryTests<Int32>FrozenDictionary_Construct1024True145,562.184 ns
DictionaryTests<Object>Dictionary_Construct1024True22,357.810 ns
DictionaryTests<Object>FrozenDictionary_Construct1024True164,976.669 ns
DictionaryTests<String>Dictionary_Construct1024True24,584.158 ns
DictionaryTests<String>FrozenDictionary_Construct1024True713,506.562 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found1024True10,972.369 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found1024True10,677.337 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found1024True6,061.961 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found1024True4,568.878 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found1024True15,521.277 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found1024True15,039.072 ns
DictionaryTests<String>Dictionary_TryGetValue_Found1024True21,252.949 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found1024True19,273.593 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound1024True10,827.871 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound1024True6,294.780 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound1024True8,168.398 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound1024True4,152.874 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound1024True14,748.903 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound1024True9,459.935 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound1024True20,727.839 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound1024True11,941.794 ns
DictionaryTests<Guid>Dictionary_Enumerate1024True3,355.945 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate1024True1,676.260 ns
DictionaryTests<Int32>Dictionary_Enumerate1024True2,501.281 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate1024True1,488.353 ns
DictionaryTests<Object>Dictionary_Enumerate1024True4,433.532 ns
DictionaryTests<Object>FrozenDictionary_Enumerate1024True3,662.125 ns
DictionaryTests<String>Dictionary_Enumerate1024True4,485.860 ns
DictionaryTests<String>FrozenDictionary_Enumerate1024True3,652.590 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys1024True2,351.717 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys1024True262.712 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys1024True2,019.313 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys1024True252.664 ns
DictionaryTests<Object>Dictionary_EnumerateKeys1024True3,673.043 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys1024True1,224.610 ns
DictionaryTests<String>Dictionary_EnumerateKeys1024True3,748.785 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys1024True1,228.282 ns
DictionaryTests<Guid>Dictionary_EnumerateValues1024True2,080.066 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues1024True255.457 ns
DictionaryTests<Int32>Dictionary_EnumerateValues1024True2,018.673 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues1024True255.488 ns
DictionaryTests<Object>Dictionary_EnumerateValues1024True2,872.265 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues1024True255.872 ns
DictionaryTests<String>Dictionary_EnumerateValues1024True2,895.610 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues1024True254.709 ns

@ghost

ghost commented Nov 2, 2022

Copy link
Copy Markdown

Note regarding the new-api-needs-documentation label:

This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, to please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change.

@ghost

ghost commented Nov 2, 2022

Copy link
Copy Markdown

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

Issue Details

The first commit imports @geeknoid's source, updated to compile in System.Collections.Immutable.dll.

The second commit overhauls the APIs and adds tests.

This includes a bunch of updates to our shared collections tests, which we've apparently not used in earnest with read-only collections, as a bunch of mutating tests were trying to run even when IsReadOnly was set to false.

There's still more work required here. Subsequent to this PR, we should:

  • Investigate ways to make construction faster.
  • Investigate ways to make lookups faster.
  • Investigate what exact special-cased implementations we want to ship.

(@geeknoid, your help here would be appreciated. Best case numbers look very good, e.g. the example benchmarks you previously created, but others don't look nearly as good. I vetted some of the worst numbers against the original implementation, and they're similar.)

Right now, for each of dictionary/set, there are custom implementations for:

  • Int32 keys with the default comparer
  • Value type keys with the default comparer
  • String keys with default/ordinal/ordinal-ignore-case comparers. Here there are two: one that tries to bucket a small number of strings by length, and one that tries to avoid reading/hashing the full strings by selecting the smallest portion that's unique enough.
  • A general fallback implementation that's used for everything else.

We can explore other data structures, other optimizations, etc., all behind the same veneer. Some of the existing implementations might not be worthwhile, too. For some situations, we might even want an implementation that just wraps Dictionary<,> along with dedicated keys/values arrays.

But the first step is getting the APIs merged to enable others to experiment.

Some benchmarks
[InProcess][GenericTypeArguments(typeof(string))][GenericTypeArguments(typeof(int))][GenericTypeArguments(typeof(object))][GenericTypeArguments(typeof(Guid))]publicclassDictionaryTests<TKey>{privateTKey[]_keys;privateTKey[]_nonExistentKeys;privateDictionary<TKey,long>_dictionary;privateFrozenDictionary<TKey,long>_frozenDictionary;[Params(1,16,1024)]publicintCount{get;set;}[Params(false,true)]publicboolDefaultComparer{get;set;}privatestaticIEqualityComparer<TKey>GetComparer(booldefaultComparer)=>defaultComparer?EqualityComparer<TKey>.Default:NonDefaultEqualityComparer<TKey>.Instance;privateunsafestaticTRandGetRandomValue<TRand>(Randomrand){if(typeof(TRand)==typeof(string)){Span<char>span=stackallocchar[rand.Next(5,20)];for(inti=0;i<span.Length;i++){span[i]=(char)('a'+rand.Next(0,26));}return(TRand)(object)span.ToString();}if(typeof(TRand)==typeof(object)){return(TRand)newobject();}if(typeof(TRand)==typeof(Guid)){Int128value=newInt128((ulong)rand.NextInt64(),(ulong)rand.NextInt64());return(TRand)(object)(*(Guid*)(&value));}if(typeof(TRand)==typeof(int)){return(TRand)(object)rand.Next();}if(typeof(TRand)==typeof(long)){return(TRand)(object)rand.NextInt64();}thrownewException("Unknown type");}[GlobalSetup]publicvoidSetup(){varrand=newRandom(42);_dictionary=newDictionary<TKey,long>(GetComparer(DefaultComparer));for(intj=0;j<Count;j++){_dictionary[GetRandomValue<TKey>(rand)]=GetRandomValue<long>(rand);}_frozenDictionary=_dictionary.ToFrozenDictionary(GetComparer(DefaultComparer));_keys=_dictionary.Keys.ToArray();if(typeof(TKey)==typeof(string)){_keys=(TKey[])(object)Array.ConvertAll((string[])(object)_keys,string.Copy);}_nonExistentKeys=newTKey[Count];for(inti=0;i<Count;i++){TKeykey;while(_dictionary.ContainsKey(key=GetRandomValue<TKey>(rand)));_nonExistentKeys[i]=key;}}[Benchmark]publicDictionary<TKey,long>Dictionary_Construct(){return_keys.ToDictionary((TKeyk)=>k,(TKeyk)=>0L,GetComparer(DefaultComparer));}[Benchmark]publicFrozenDictionary<TKey,long>FrozenDictionary_Construct(){return_keys.ToFrozenDictionary((TKeyk)=>k,(TKeyk)=>0L,GetComparer(DefaultComparer));}[Benchmark]publicboolDictionary_TryGetValue_Found(){boolallFound=true;TKey[]keys=_keys;foreach(TKeykeyinkeys){longvalue;allFound&=_dictionary.TryGetValue(key,outvalue);}returnallFound;}[Benchmark]publicboolDictionary_TryGetValue_NotFound(){boolanyFound=false;TKey[]nonExistentKeys=_nonExistentKeys;foreach(TKeykeyinnonExistentKeys){longvalue;anyFound|=_dictionary.TryGetValue(key,outvalue);}returnanyFound;}[Benchmark]publicboolFrozenDictionary_TryGetValue_Found(){boolallFound=true;TKey[]keys=_keys;foreach(TKeykeyinkeys){longvalue;allFound&=_frozenDictionary.TryGetValue(key,outvalue);}returnallFound;}[Benchmark]publicboolFrozenDictionary_TryGetValue_NotFound(){boolanyFound=false;TKey[]nonExistentKeys=_nonExistentKeys;foreach(TKeykeyinnonExistentKeys){longvalue;anyFound|=_frozenDictionary.TryGetValue(key,outvalue);}returnanyFound;}[Benchmark]publicintDictionary_Enumerate(){intcount=0;foreach(KeyValuePair<TKey,long>itemin_dictionary){_=item;count++;}returncount;}[Benchmark]publicintDictionary_EnumerateKeys(){intcount=0;foreach(TKeykeyin_dictionary.Keys){_=key;count++;}returncount;}[Benchmark]publiclongDictionary_EnumerateValues(){intcount=0;foreach(longvaluein_dictionary.Values){_=value;count++;}returncount;}[Benchmark]publicintFrozenDictionary_Enumerate(){intcount=0;foreach(KeyValuePair<TKey,long>itemin_frozenDictionary){_=item;count++;}returncount;}[Benchmark]publicintFrozenDictionary_EnumerateKeys(){intcount=0;foreach(TKeykeyin_frozenDictionary.Keys){count++;}returncount;}[Benchmark]publicintFrozenDictionary_EnumerateValues(){intcount=0;foreach(longvaluein_frozenDictionary.Values){count++;}returncount;}}internalsealedpartialclassNonDefaultEqualityComparer<T>:EqualityComparer<T>{publicstaticNonDefaultEqualityComparer<T>Instance{get;}=newNonDefaultEqualityComparer<T>();publicoverrideboolEquals(Tx,Ty)=>Default.Equals(x,y);publicoverrideintGetHashCode(Tobj)=>Default.GetHashCode(obj);}
TypeMethodCountDefaultComparerMean
DictionaryTests<Guid>Dictionary_Construct16False308.917 ns
DictionaryTests<Guid>FrozenDictionary_Construct16False2,546.966 ns
DictionaryTests<Int32>Dictionary_Construct16False256.919 ns
DictionaryTests<Int32>FrozenDictionary_Construct16False2,735.513 ns
DictionaryTests<Object>Dictionary_Construct16False459.599 ns
DictionaryTests<Object>FrozenDictionary_Construct16False2,990.372 ns
DictionaryTests<String>Dictionary_Construct16False574.025 ns
DictionaryTests<String>FrozenDictionary_Construct16False3,085.790 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found16False195.613 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found16False180.628 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found16False127.828 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found16False120.595 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found16False371.343 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found16False384.100 ns
DictionaryTests<String>Dictionary_TryGetValue_Found16False559.943 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found16False531.615 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound16False149.167 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound16False120.109 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound16False109.596 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound16False91.319 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound16False247.097 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound16False231.592 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound16False356.568 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound16False334.678 ns
DictionaryTests<Guid>Dictionary_Enumerate16False61.120 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate16False27.948 ns
DictionaryTests<Int32>Dictionary_Enumerate16False50.372 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate16False27.162 ns
DictionaryTests<Object>Dictionary_Enumerate16False89.827 ns
DictionaryTests<Object>FrozenDictionary_Enumerate16False74.392 ns
DictionaryTests<String>Dictionary_Enumerate16False89.446 ns
DictionaryTests<String>FrozenDictionary_Enumerate16False74.626 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys16False49.057 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys16False4.847 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys16False37.000 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys16False5.108 ns
DictionaryTests<Object>Dictionary_EnumerateKeys16False74.497 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys16False18.488 ns
DictionaryTests<String>Dictionary_EnumerateKeys16False74.906 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys16False18.571 ns
DictionaryTests<Guid>Dictionary_EnumerateValues16False38.145 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues16False5.054 ns
DictionaryTests<Int32>Dictionary_EnumerateValues16False36.834 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues16False5.103 ns
DictionaryTests<Object>Dictionary_EnumerateValues16False54.844 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues16False5.045 ns
DictionaryTests<String>Dictionary_EnumerateValues16False55.299 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues16False5.048 ns
DictionaryTests<Guid>Dictionary_Construct16True276.233 ns
DictionaryTests<Guid>FrozenDictionary_Construct16True2,480.023 ns
DictionaryTests<Int32>Dictionary_Construct16True247.043 ns
DictionaryTests<Int32>FrozenDictionary_Construct16True2,679.061 ns
DictionaryTests<Object>Dictionary_Construct16True401.734 ns
DictionaryTests<Object>FrozenDictionary_Construct16True2,782.085 ns
DictionaryTests<String>Dictionary_Construct16True360.930 ns
DictionaryTests<String>FrozenDictionary_Construct16True1,385.360 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found16True116.494 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found16True124.232 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found16True87.759 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found16True50.979 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found16True207.784 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found16True209.461 ns
DictionaryTests<String>Dictionary_TryGetValue_Found16True268.939 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found16True142.170 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound16True99.116 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound16True81.180 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound16True72.956 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound16True53.239 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound16True163.937 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound16True144.715 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound16True178.219 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound16True109.456 ns
DictionaryTests<Guid>Dictionary_Enumerate16True58.001 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate16True30.898 ns
DictionaryTests<Int32>Dictionary_Enumerate16True49.878 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate16True27.879 ns
DictionaryTests<Object>Dictionary_Enumerate16True89.847 ns
DictionaryTests<Object>FrozenDictionary_Enumerate16True74.568 ns
DictionaryTests<String>Dictionary_Enumerate16True89.347 ns
DictionaryTests<String>FrozenDictionary_Enumerate16True73.377 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys16True48.112 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys16True5.259 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys16True36.038 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys16True5.016 ns
DictionaryTests<Object>Dictionary_EnumerateKeys16True74.331 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys16True19.002 ns
DictionaryTests<String>Dictionary_EnumerateKeys16True73.520 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys16True18.961 ns
DictionaryTests<Guid>Dictionary_EnumerateValues16True39.389 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues16True5.432 ns
DictionaryTests<Int32>Dictionary_EnumerateValues16True37.072 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues16True4.999 ns
DictionaryTests<Object>Dictionary_EnumerateValues16True54.973 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues16True5.033 ns
DictionaryTests<String>Dictionary_EnumerateValues16True54.496 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues16True5.029 ns
DictionaryTests<Guid>Dictionary_Construct1024False20,251.116 ns
DictionaryTests<Guid>FrozenDictionary_Construct1024False165,973.254 ns
DictionaryTests<Int32>Dictionary_Construct1024False16,858.288 ns
DictionaryTests<Int32>FrozenDictionary_Construct1024False149,391.455 ns
DictionaryTests<Object>Dictionary_Construct1024False26,154.444 ns
DictionaryTests<Object>FrozenDictionary_Construct1024False174,545.846 ns
DictionaryTests<String>Dictionary_Construct1024False36,381.752 ns
DictionaryTests<String>FrozenDictionary_Construct1024False200,478.010 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found1024False16,232.339 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found1024False14,450.573 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found1024False10,903.514 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found1024False9,294.073 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found1024False24,834.107 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found1024False27,807.133 ns
DictionaryTests<String>Dictionary_TryGetValue_Found1024False43,093.660 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found1024False39,181.201 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound1024False13,773.527 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound1024False8,281.616 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound1024False10,252.792 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound1024False6,199.940 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound1024False20,427.130 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound1024False15,131.280 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound1024False26,598.257 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound1024False24,474.515 ns
DictionaryTests<Guid>Dictionary_Enumerate1024False3,440.145 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate1024False1,689.686 ns
DictionaryTests<Int32>Dictionary_Enumerate1024False2,520.821 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate1024False1,498.062 ns
DictionaryTests<Object>Dictionary_Enumerate1024False4,408.578 ns
DictionaryTests<Object>FrozenDictionary_Enumerate1024False3,689.598 ns
DictionaryTests<String>Dictionary_Enumerate1024False4,398.440 ns
DictionaryTests<String>FrozenDictionary_Enumerate1024False3,636.864 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys1024False2,359.947 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys1024False261.473 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys1024False2,033.189 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys1024False253.029 ns
DictionaryTests<Object>Dictionary_EnumerateKeys1024False3,742.122 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys1024False1,226.121 ns
DictionaryTests<String>Dictionary_EnumerateKeys1024False3,693.788 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys1024False1,222.912 ns
DictionaryTests<Guid>Dictionary_EnumerateValues1024False2,144.243 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues1024False260.725 ns
DictionaryTests<Int32>Dictionary_EnumerateValues1024False2,039.401 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues1024False255.137 ns
DictionaryTests<Object>Dictionary_EnumerateValues1024False2,900.210 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues1024False254.610 ns
DictionaryTests<String>Dictionary_EnumerateValues1024False2,924.998 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues1024False252.582 ns
DictionaryTests<Guid>Dictionary_Construct1024True17,653.020 ns
DictionaryTests<Guid>FrozenDictionary_Construct1024True165,900.270 ns
DictionaryTests<Int32>Dictionary_Construct1024True15,324.775 ns
DictionaryTests<Int32>FrozenDictionary_Construct1024True145,562.184 ns
DictionaryTests<Object>Dictionary_Construct1024True22,357.810 ns
DictionaryTests<Object>FrozenDictionary_Construct1024True164,976.669 ns
DictionaryTests<String>Dictionary_Construct1024True24,584.158 ns
DictionaryTests<String>FrozenDictionary_Construct1024True713,506.562 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found1024True10,972.369 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found1024True10,677.337 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found1024True6,061.961 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found1024True4,568.878 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found1024True15,521.277 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found1024True15,039.072 ns
DictionaryTests<String>Dictionary_TryGetValue_Found1024True21,252.949 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found1024True19,273.593 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound1024True10,827.871 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound1024True6,294.780 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound1024True8,168.398 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound1024True4,152.874 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound1024True14,748.903 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound1024True9,459.935 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound1024True20,727.839 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound1024True11,941.794 ns
DictionaryTests<Guid>Dictionary_Enumerate1024True3,355.945 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate1024True1,676.260 ns
DictionaryTests<Int32>Dictionary_Enumerate1024True2,501.281 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate1024True1,488.353 ns
DictionaryTests<Object>Dictionary_Enumerate1024True4,433.532 ns
DictionaryTests<Object>FrozenDictionary_Enumerate1024True3,662.125 ns
DictionaryTests<String>Dictionary_Enumerate1024True4,485.860 ns
DictionaryTests<String>FrozenDictionary_Enumerate1024True3,652.590 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys1024True2,351.717 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys1024True262.712 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys1024True2,019.313 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys1024True252.664 ns
DictionaryTests<Object>Dictionary_EnumerateKeys1024True3,673.043 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys1024True1,224.610 ns
DictionaryTests<String>Dictionary_EnumerateKeys1024True3,748.785 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys1024True1,228.282 ns
DictionaryTests<Guid>Dictionary_EnumerateValues1024True2,080.066 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues1024True255.457 ns
DictionaryTests<Int32>Dictionary_EnumerateValues1024True2,018.673 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues1024True255.488 ns
DictionaryTests<Object>Dictionary_EnumerateValues1024True2,872.265 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues1024True255.872 ns
DictionaryTests<String>Dictionary_EnumerateValues1024True2,895.610 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues1024True254.709 ns
Author:stephentoub
Assignees:-
Labels:

area-System.Collections

Milestone:8.0.0

@ghostghost assigned stephentoubNov 2, 2022
@geeknoid

Copy link
Copy Markdown
Member

The read benchmarks look pretty good, but the construction phase is definitely on the slow side. I never really measure the construction time since for my scenarios I didn't care so much. I do think adding some notion of "intensity" in the constructor to let the caller decide how much time to spend optimizing would be desirable.

It would be interesting in particular to see what the read perf is if the implementation doesn't perform any optimization at all. It would likely still be faster than a normal Dictionary/HashSet when using the default comparers, but not by much. But if that case is still a bit faster and construction time is on par with normal Dictionary/HashSet, then that says that any scenario that needs a readonly dictionary or set should be using the frozen ones by default since it would be faster.

Without the ability to tune how much time is spent constructing, then you have to be careful where you use the frozen collections and make sure the cost/benefit will be worth it.

@geeknoid

geeknoid commented Nov 2, 2022

Copy link
Copy Markdown
Member

Some thoughts for the future:

  • Dictionaries and sets with low cardinality are common. We should look at adding specialized implementation for 1 and 2 entry collections. This saves memory and speeds things up.

  • I had on my todo list to create specialized two character string comparers to optimize that case. For small collections, often the single char comparer is enough and works well. But when the collection grows, you run out of single characters to compare, so the code falls back to two.

  • We discussed in the review that the majority use case for this kind of collection is where the data comes dynamically into the process. This says that doing the optimization work for the collections in a source generator is not likely to be used frequently. However, what I think might be useful is the ability to serialize/deserialize the internal state for the frozen collections in a portable way. This would let me distribute this serialized state as config, and thus forego the construction overhead. So instead of putting a bunch of key/value pairs in my config and having to optimize those in each of my service's processes, I could optimize the key/value pairs offline and push the resulting state to the processes.

stephentouband others added 2 commits November 2, 2022 17:45
…y compile
Co-authored-by: Martin Taillefer <mataille@microsoft.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>

@geeknoidgeeknoid left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This looks great.

@krwq

krwq commented Nov 4, 2022

Copy link
Copy Markdown
Member

I'd be really interested to see how integration of this feature would look like with System.Text.Json (both JsonSerializerOptions.Converters and JsonTypeInfo.Properties are freezing values at one point)

krwq
krwq approved these changes Nov 4, 2022

@krwqkrwq left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I've skimmed all of the files and overall looks good. I'm really impressed in the amount of heuristics done here to make this super optimized. Thank you!

@stephentoub
stephentoub merged commit 6c5a440 into dotnet:mainNov 4, 2022
@stephentoub
stephentoub deleted the frozencollections branch November 4, 2022 19:49
@ghostghost locked as resolved and limited conversation to collaborators Dec 5, 2022
@jeffhandleyjeffhandley added the blog-candidate Completed PRs that are candidate topics for blog post coverage label Mar 14, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Collectionsblog-candidateCompleted PRs that are candidate topics for blog post coveragenew-api-needs-documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[API Proposal]: Provide optimized read-only collections

4 participants

@stephentoub@geeknoid@krwq@jeffhandley
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add FrozenDictionary/Set - #77799

Merged
stephentoub merged 3 commits into
dotnet:mainfrom
stephentoub:frozencollections
Nov 4, 2022
Merged

Add FrozenDictionary/Set#77799
stephentoub merged 3 commits into
dotnet:mainfrom
stephentoub:frozencollections

Conversation

@stephentoub

@stephentoubstephentoub commented Nov 2, 2022

Copy link
Copy Markdown
Member

Fixes#67209

The first commit imports @geeknoid's source, updated to compile in System.Collections.Immutable.dll.

The second commit overhauls the APIs and adds tests. Overhauling the APIs also required lots of surgery on various aspects of the implementation.

This includes a bunch of updates to our shared collections tests, which we've apparently not used in earnest with read-only collections, as a bunch of mutating tests were trying to run even when IsReadOnly was set to false.

There's still more work required here. Subsequent to this PR, we should:

  • Investigate ways to make construction faster.
  • Investigate ways to make lookups faster.
  • Investigate what exact special-cased implementations we want to ship.

(@geeknoid, your help here would be appreciated. Best case numbers look very good, e.g. the example benchmarks you previously created, but others don't look nearly as good. I vetted some of the worst numbers against the original implementation, and they're similar.)

Right now, for each of dictionary/set, there are custom implementations for:

  • Int32 keys with the default comparer
  • Value type keys with the default comparer
  • String keys with default/ordinal/ordinal-ignore-case comparers. Here there are two: one that tries to bucket a small number of strings by length, and one that tries to avoid reading/hashing the full strings by selecting the smallest portion that's unique enough.
  • A general fallback implementation that's used for everything else.

We can explore other data structures, other optimizations, etc., all behind the same veneer. Some of the existing implementations might not be worthwhile, too. For some situations, we might even want an implementation that just wraps Dictionary<,> along with dedicated keys/values arrays.

But the first step is getting the APIs merged to enable others to experiment.

Some benchmarks
[InProcess][GenericTypeArguments(typeof(string))][GenericTypeArguments(typeof(int))][GenericTypeArguments(typeof(object))][GenericTypeArguments(typeof(Guid))]publicclassDictionaryTests<TKey>{privateTKey[]_keys;privateTKey[]_nonExistentKeys;privateDictionary<TKey,long>_dictionary;privateFrozenDictionary<TKey,long>_frozenDictionary;[Params(1,16,1024)]publicintCount{get;set;}[Params(false,true)]publicboolDefaultComparer{get;set;}privatestaticIEqualityComparer<TKey>GetComparer(booldefaultComparer)=>defaultComparer?EqualityComparer<TKey>.Default:NonDefaultEqualityComparer<TKey>.Instance;privateunsafestaticTRandGetRandomValue<TRand>(Randomrand){if(typeof(TRand)==typeof(string)){Span<char>span=stackallocchar[rand.Next(5,20)];for(inti=0;i<span.Length;i++){span[i]=(char)('a'+rand.Next(0,26));}return(TRand)(object)span.ToString();}if(typeof(TRand)==typeof(object)){return(TRand)newobject();}if(typeof(TRand)==typeof(Guid)){Int128value=newInt128((ulong)rand.NextInt64(),(ulong)rand.NextInt64());return(TRand)(object)(*(Guid*)(&value));}if(typeof(TRand)==typeof(int)){return(TRand)(object)rand.Next();}if(typeof(TRand)==typeof(long)){return(TRand)(object)rand.NextInt64();}thrownewException("Unknown type");}[GlobalSetup]publicvoidSetup(){varrand=newRandom(42);_dictionary=newDictionary<TKey,long>(GetComparer(DefaultComparer));for(intj=0;j<Count;j++){_dictionary[GetRandomValue<TKey>(rand)]=GetRandomValue<long>(rand);}_frozenDictionary=_dictionary.ToFrozenDictionary(GetComparer(DefaultComparer));_keys=_dictionary.Keys.ToArray();if(typeof(TKey)==typeof(string)){_keys=(TKey[])(object)Array.ConvertAll((string[])(object)_keys,string.Copy);}_nonExistentKeys=newTKey[Count];for(inti=0;i<Count;i++){TKeykey;while(_dictionary.ContainsKey(key=GetRandomValue<TKey>(rand)));_nonExistentKeys[i]=key;}}[Benchmark]publicDictionary<TKey,long>Dictionary_Construct(){return_keys.ToDictionary((TKeyk)=>k,(TKeyk)=>0L,GetComparer(DefaultComparer));}[Benchmark]publicFrozenDictionary<TKey,long>FrozenDictionary_Construct(){return_keys.ToFrozenDictionary((TKeyk)=>k,(TKeyk)=>0L,GetComparer(DefaultComparer));}[Benchmark]publicboolDictionary_TryGetValue_Found(){boolallFound=true;TKey[]keys=_keys;foreach(TKeykeyinkeys){longvalue;allFound&=_dictionary.TryGetValue(key,outvalue);}returnallFound;}[Benchmark]publicboolDictionary_TryGetValue_NotFound(){boolanyFound=false;TKey[]nonExistentKeys=_nonExistentKeys;foreach(TKeykeyinnonExistentKeys){longvalue;anyFound|=_dictionary.TryGetValue(key,outvalue);}returnanyFound;}[Benchmark]publicboolFrozenDictionary_TryGetValue_Found(){boolallFound=true;TKey[]keys=_keys;foreach(TKeykeyinkeys){longvalue;allFound&=_frozenDictionary.TryGetValue(key,outvalue);}returnallFound;}[Benchmark]publicboolFrozenDictionary_TryGetValue_NotFound(){boolanyFound=false;TKey[]nonExistentKeys=_nonExistentKeys;foreach(TKeykeyinnonExistentKeys){longvalue;anyFound|=_frozenDictionary.TryGetValue(key,outvalue);}returnanyFound;}[Benchmark]publicintDictionary_Enumerate(){intcount=0;foreach(KeyValuePair<TKey,long>itemin_dictionary){_=item;count++;}returncount;}[Benchmark]publicintDictionary_EnumerateKeys(){intcount=0;foreach(TKeykeyin_dictionary.Keys){_=key;count++;}returncount;}[Benchmark]publiclongDictionary_EnumerateValues(){intcount=0;foreach(longvaluein_dictionary.Values){_=value;count++;}returncount;}[Benchmark]publicintFrozenDictionary_Enumerate(){intcount=0;foreach(KeyValuePair<TKey,long>itemin_frozenDictionary){_=item;count++;}returncount;}[Benchmark]publicintFrozenDictionary_EnumerateKeys(){intcount=0;foreach(TKeykeyin_frozenDictionary.Keys){count++;}returncount;}[Benchmark]publicintFrozenDictionary_EnumerateValues(){intcount=0;foreach(longvaluein_frozenDictionary.Values){count++;}returncount;}}internalsealedpartialclassNonDefaultEqualityComparer<T>:EqualityComparer<T>{publicstaticNonDefaultEqualityComparer<T>Instance{get;}=newNonDefaultEqualityComparer<T>();publicoverrideboolEquals(Tx,Ty)=>Default.Equals(x,y);publicoverrideintGetHashCode(Tobj)=>Default.GetHashCode(obj);}
TypeMethodCountDefaultComparerMean
DictionaryTests<Guid>Dictionary_Construct16False308.917 ns
DictionaryTests<Guid>FrozenDictionary_Construct16False2,546.966 ns
DictionaryTests<Int32>Dictionary_Construct16False256.919 ns
DictionaryTests<Int32>FrozenDictionary_Construct16False2,735.513 ns
DictionaryTests<Object>Dictionary_Construct16False459.599 ns
DictionaryTests<Object>FrozenDictionary_Construct16False2,990.372 ns
DictionaryTests<String>Dictionary_Construct16False574.025 ns
DictionaryTests<String>FrozenDictionary_Construct16False3,085.790 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found16False195.613 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found16False180.628 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found16False127.828 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found16False120.595 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found16False371.343 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found16False384.100 ns
DictionaryTests<String>Dictionary_TryGetValue_Found16False559.943 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found16False531.615 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound16False149.167 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound16False120.109 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound16False109.596 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound16False91.319 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound16False247.097 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound16False231.592 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound16False356.568 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound16False334.678 ns
DictionaryTests<Guid>Dictionary_Enumerate16False61.120 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate16False27.948 ns
DictionaryTests<Int32>Dictionary_Enumerate16False50.372 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate16False27.162 ns
DictionaryTests<Object>Dictionary_Enumerate16False89.827 ns
DictionaryTests<Object>FrozenDictionary_Enumerate16False74.392 ns
DictionaryTests<String>Dictionary_Enumerate16False89.446 ns
DictionaryTests<String>FrozenDictionary_Enumerate16False74.626 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys16False49.057 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys16False4.847 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys16False37.000 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys16False5.108 ns
DictionaryTests<Object>Dictionary_EnumerateKeys16False74.497 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys16False18.488 ns
DictionaryTests<String>Dictionary_EnumerateKeys16False74.906 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys16False18.571 ns
DictionaryTests<Guid>Dictionary_EnumerateValues16False38.145 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues16False5.054 ns
DictionaryTests<Int32>Dictionary_EnumerateValues16False36.834 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues16False5.103 ns
DictionaryTests<Object>Dictionary_EnumerateValues16False54.844 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues16False5.045 ns
DictionaryTests<String>Dictionary_EnumerateValues16False55.299 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues16False5.048 ns
DictionaryTests<Guid>Dictionary_Construct16True276.233 ns
DictionaryTests<Guid>FrozenDictionary_Construct16True2,480.023 ns
DictionaryTests<Int32>Dictionary_Construct16True247.043 ns
DictionaryTests<Int32>FrozenDictionary_Construct16True2,679.061 ns
DictionaryTests<Object>Dictionary_Construct16True401.734 ns
DictionaryTests<Object>FrozenDictionary_Construct16True2,782.085 ns
DictionaryTests<String>Dictionary_Construct16True360.930 ns
DictionaryTests<String>FrozenDictionary_Construct16True1,385.360 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found16True116.494 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found16True124.232 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found16True87.759 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found16True50.979 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found16True207.784 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found16True209.461 ns
DictionaryTests<String>Dictionary_TryGetValue_Found16True268.939 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found16True142.170 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound16True99.116 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound16True81.180 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound16True72.956 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound16True53.239 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound16True163.937 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound16True144.715 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound16True178.219 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound16True109.456 ns
DictionaryTests<Guid>Dictionary_Enumerate16True58.001 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate16True30.898 ns
DictionaryTests<Int32>Dictionary_Enumerate16True49.878 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate16True27.879 ns
DictionaryTests<Object>Dictionary_Enumerate16True89.847 ns
DictionaryTests<Object>FrozenDictionary_Enumerate16True74.568 ns
DictionaryTests<String>Dictionary_Enumerate16True89.347 ns
DictionaryTests<String>FrozenDictionary_Enumerate16True73.377 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys16True48.112 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys16True5.259 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys16True36.038 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys16True5.016 ns
DictionaryTests<Object>Dictionary_EnumerateKeys16True74.331 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys16True19.002 ns
DictionaryTests<String>Dictionary_EnumerateKeys16True73.520 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys16True18.961 ns
DictionaryTests<Guid>Dictionary_EnumerateValues16True39.389 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues16True5.432 ns
DictionaryTests<Int32>Dictionary_EnumerateValues16True37.072 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues16True4.999 ns
DictionaryTests<Object>Dictionary_EnumerateValues16True54.973 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues16True5.033 ns
DictionaryTests<String>Dictionary_EnumerateValues16True54.496 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues16True5.029 ns
DictionaryTests<Guid>Dictionary_Construct1024False20,251.116 ns
DictionaryTests<Guid>FrozenDictionary_Construct1024False165,973.254 ns
DictionaryTests<Int32>Dictionary_Construct1024False16,858.288 ns
DictionaryTests<Int32>FrozenDictionary_Construct1024False149,391.455 ns
DictionaryTests<Object>Dictionary_Construct1024False26,154.444 ns
DictionaryTests<Object>FrozenDictionary_Construct1024False174,545.846 ns
DictionaryTests<String>Dictionary_Construct1024False36,381.752 ns
DictionaryTests<String>FrozenDictionary_Construct1024False200,478.010 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found1024False16,232.339 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found1024False14,450.573 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found1024False10,903.514 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found1024False9,294.073 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found1024False24,834.107 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found1024False27,807.133 ns
DictionaryTests<String>Dictionary_TryGetValue_Found1024False43,093.660 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found1024False39,181.201 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound1024False13,773.527 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound1024False8,281.616 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound1024False10,252.792 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound1024False6,199.940 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound1024False20,427.130 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound1024False15,131.280 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound1024False26,598.257 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound1024False24,474.515 ns
DictionaryTests<Guid>Dictionary_Enumerate1024False3,440.145 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate1024False1,689.686 ns
DictionaryTests<Int32>Dictionary_Enumerate1024False2,520.821 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate1024False1,498.062 ns
DictionaryTests<Object>Dictionary_Enumerate1024False4,408.578 ns
DictionaryTests<Object>FrozenDictionary_Enumerate1024False3,689.598 ns
DictionaryTests<String>Dictionary_Enumerate1024False4,398.440 ns
DictionaryTests<String>FrozenDictionary_Enumerate1024False3,636.864 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys1024False2,359.947 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys1024False261.473 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys1024False2,033.189 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys1024False253.029 ns
DictionaryTests<Object>Dictionary_EnumerateKeys1024False3,742.122 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys1024False1,226.121 ns
DictionaryTests<String>Dictionary_EnumerateKeys1024False3,693.788 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys1024False1,222.912 ns
DictionaryTests<Guid>Dictionary_EnumerateValues1024False2,144.243 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues1024False260.725 ns
DictionaryTests<Int32>Dictionary_EnumerateValues1024False2,039.401 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues1024False255.137 ns
DictionaryTests<Object>Dictionary_EnumerateValues1024False2,900.210 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues1024False254.610 ns
DictionaryTests<String>Dictionary_EnumerateValues1024False2,924.998 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues1024False252.582 ns
DictionaryTests<Guid>Dictionary_Construct1024True17,653.020 ns
DictionaryTests<Guid>FrozenDictionary_Construct1024True165,900.270 ns
DictionaryTests<Int32>Dictionary_Construct1024True15,324.775 ns
DictionaryTests<Int32>FrozenDictionary_Construct1024True145,562.184 ns
DictionaryTests<Object>Dictionary_Construct1024True22,357.810 ns
DictionaryTests<Object>FrozenDictionary_Construct1024True164,976.669 ns
DictionaryTests<String>Dictionary_Construct1024True24,584.158 ns
DictionaryTests<String>FrozenDictionary_Construct1024True713,506.562 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found1024True10,972.369 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found1024True10,677.337 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found1024True6,061.961 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found1024True4,568.878 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found1024True15,521.277 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found1024True15,039.072 ns
DictionaryTests<String>Dictionary_TryGetValue_Found1024True21,252.949 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found1024True19,273.593 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound1024True10,827.871 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound1024True6,294.780 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound1024True8,168.398 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound1024True4,152.874 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound1024True14,748.903 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound1024True9,459.935 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound1024True20,727.839 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound1024True11,941.794 ns
DictionaryTests<Guid>Dictionary_Enumerate1024True3,355.945 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate1024True1,676.260 ns
DictionaryTests<Int32>Dictionary_Enumerate1024True2,501.281 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate1024True1,488.353 ns
DictionaryTests<Object>Dictionary_Enumerate1024True4,433.532 ns
DictionaryTests<Object>FrozenDictionary_Enumerate1024True3,662.125 ns
DictionaryTests<String>Dictionary_Enumerate1024True4,485.860 ns
DictionaryTests<String>FrozenDictionary_Enumerate1024True3,652.590 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys1024True2,351.717 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys1024True262.712 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys1024True2,019.313 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys1024True252.664 ns
DictionaryTests<Object>Dictionary_EnumerateKeys1024True3,673.043 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys1024True1,224.610 ns
DictionaryTests<String>Dictionary_EnumerateKeys1024True3,748.785 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys1024True1,228.282 ns
DictionaryTests<Guid>Dictionary_EnumerateValues1024True2,080.066 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues1024True255.457 ns
DictionaryTests<Int32>Dictionary_EnumerateValues1024True2,018.673 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues1024True255.488 ns
DictionaryTests<Object>Dictionary_EnumerateValues1024True2,872.265 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues1024True255.872 ns
DictionaryTests<String>Dictionary_EnumerateValues1024True2,895.610 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues1024True254.709 ns

@ghost

ghost commented Nov 2, 2022

Copy link
Copy Markdown

Note regarding the new-api-needs-documentation label:

This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, to please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change.

@ghost

ghost commented Nov 2, 2022

Copy link
Copy Markdown

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

Issue Details

The first commit imports @geeknoid's source, updated to compile in System.Collections.Immutable.dll.

The second commit overhauls the APIs and adds tests.

This includes a bunch of updates to our shared collections tests, which we've apparently not used in earnest with read-only collections, as a bunch of mutating tests were trying to run even when IsReadOnly was set to false.

There's still more work required here. Subsequent to this PR, we should:

  • Investigate ways to make construction faster.
  • Investigate ways to make lookups faster.
  • Investigate what exact special-cased implementations we want to ship.

(@geeknoid, your help here would be appreciated. Best case numbers look very good, e.g. the example benchmarks you previously created, but others don't look nearly as good. I vetted some of the worst numbers against the original implementation, and they're similar.)

Right now, for each of dictionary/set, there are custom implementations for:

  • Int32 keys with the default comparer
  • Value type keys with the default comparer
  • String keys with default/ordinal/ordinal-ignore-case comparers. Here there are two: one that tries to bucket a small number of strings by length, and one that tries to avoid reading/hashing the full strings by selecting the smallest portion that's unique enough.
  • A general fallback implementation that's used for everything else.

We can explore other data structures, other optimizations, etc., all behind the same veneer. Some of the existing implementations might not be worthwhile, too. For some situations, we might even want an implementation that just wraps Dictionary<,> along with dedicated keys/values arrays.

But the first step is getting the APIs merged to enable others to experiment.

Some benchmarks
[InProcess][GenericTypeArguments(typeof(string))][GenericTypeArguments(typeof(int))][GenericTypeArguments(typeof(object))][GenericTypeArguments(typeof(Guid))]publicclassDictionaryTests<TKey>{privateTKey[]_keys;privateTKey[]_nonExistentKeys;privateDictionary<TKey,long>_dictionary;privateFrozenDictionary<TKey,long>_frozenDictionary;[Params(1,16,1024)]publicintCount{get;set;}[Params(false,true)]publicboolDefaultComparer{get;set;}privatestaticIEqualityComparer<TKey>GetComparer(booldefaultComparer)=>defaultComparer?EqualityComparer<TKey>.Default:NonDefaultEqualityComparer<TKey>.Instance;privateunsafestaticTRandGetRandomValue<TRand>(Randomrand){if(typeof(TRand)==typeof(string)){Span<char>span=stackallocchar[rand.Next(5,20)];for(inti=0;i<span.Length;i++){span[i]=(char)('a'+rand.Next(0,26));}return(TRand)(object)span.ToString();}if(typeof(TRand)==typeof(object)){return(TRand)newobject();}if(typeof(TRand)==typeof(Guid)){Int128value=newInt128((ulong)rand.NextInt64(),(ulong)rand.NextInt64());return(TRand)(object)(*(Guid*)(&value));}if(typeof(TRand)==typeof(int)){return(TRand)(object)rand.Next();}if(typeof(TRand)==typeof(long)){return(TRand)(object)rand.NextInt64();}thrownewException("Unknown type");}[GlobalSetup]publicvoidSetup(){varrand=newRandom(42);_dictionary=newDictionary<TKey,long>(GetComparer(DefaultComparer));for(intj=0;j<Count;j++){_dictionary[GetRandomValue<TKey>(rand)]=GetRandomValue<long>(rand);}_frozenDictionary=_dictionary.ToFrozenDictionary(GetComparer(DefaultComparer));_keys=_dictionary.Keys.ToArray();if(typeof(TKey)==typeof(string)){_keys=(TKey[])(object)Array.ConvertAll((string[])(object)_keys,string.Copy);}_nonExistentKeys=newTKey[Count];for(inti=0;i<Count;i++){TKeykey;while(_dictionary.ContainsKey(key=GetRandomValue<TKey>(rand)));_nonExistentKeys[i]=key;}}[Benchmark]publicDictionary<TKey,long>Dictionary_Construct(){return_keys.ToDictionary((TKeyk)=>k,(TKeyk)=>0L,GetComparer(DefaultComparer));}[Benchmark]publicFrozenDictionary<TKey,long>FrozenDictionary_Construct(){return_keys.ToFrozenDictionary((TKeyk)=>k,(TKeyk)=>0L,GetComparer(DefaultComparer));}[Benchmark]publicboolDictionary_TryGetValue_Found(){boolallFound=true;TKey[]keys=_keys;foreach(TKeykeyinkeys){longvalue;allFound&=_dictionary.TryGetValue(key,outvalue);}returnallFound;}[Benchmark]publicboolDictionary_TryGetValue_NotFound(){boolanyFound=false;TKey[]nonExistentKeys=_nonExistentKeys;foreach(TKeykeyinnonExistentKeys){longvalue;anyFound|=_dictionary.TryGetValue(key,outvalue);}returnanyFound;}[Benchmark]publicboolFrozenDictionary_TryGetValue_Found(){boolallFound=true;TKey[]keys=_keys;foreach(TKeykeyinkeys){longvalue;allFound&=_frozenDictionary.TryGetValue(key,outvalue);}returnallFound;}[Benchmark]publicboolFrozenDictionary_TryGetValue_NotFound(){boolanyFound=false;TKey[]nonExistentKeys=_nonExistentKeys;foreach(TKeykeyinnonExistentKeys){longvalue;anyFound|=_frozenDictionary.TryGetValue(key,outvalue);}returnanyFound;}[Benchmark]publicintDictionary_Enumerate(){intcount=0;foreach(KeyValuePair<TKey,long>itemin_dictionary){_=item;count++;}returncount;}[Benchmark]publicintDictionary_EnumerateKeys(){intcount=0;foreach(TKeykeyin_dictionary.Keys){_=key;count++;}returncount;}[Benchmark]publiclongDictionary_EnumerateValues(){intcount=0;foreach(longvaluein_dictionary.Values){_=value;count++;}returncount;}[Benchmark]publicintFrozenDictionary_Enumerate(){intcount=0;foreach(KeyValuePair<TKey,long>itemin_frozenDictionary){_=item;count++;}returncount;}[Benchmark]publicintFrozenDictionary_EnumerateKeys(){intcount=0;foreach(TKeykeyin_frozenDictionary.Keys){count++;}returncount;}[Benchmark]publicintFrozenDictionary_EnumerateValues(){intcount=0;foreach(longvaluein_frozenDictionary.Values){count++;}returncount;}}internalsealedpartialclassNonDefaultEqualityComparer<T>:EqualityComparer<T>{publicstaticNonDefaultEqualityComparer<T>Instance{get;}=newNonDefaultEqualityComparer<T>();publicoverrideboolEquals(Tx,Ty)=>Default.Equals(x,y);publicoverrideintGetHashCode(Tobj)=>Default.GetHashCode(obj);}
TypeMethodCountDefaultComparerMean
DictionaryTests<Guid>Dictionary_Construct16False308.917 ns
DictionaryTests<Guid>FrozenDictionary_Construct16False2,546.966 ns
DictionaryTests<Int32>Dictionary_Construct16False256.919 ns
DictionaryTests<Int32>FrozenDictionary_Construct16False2,735.513 ns
DictionaryTests<Object>Dictionary_Construct16False459.599 ns
DictionaryTests<Object>FrozenDictionary_Construct16False2,990.372 ns
DictionaryTests<String>Dictionary_Construct16False574.025 ns
DictionaryTests<String>FrozenDictionary_Construct16False3,085.790 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found16False195.613 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found16False180.628 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found16False127.828 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found16False120.595 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found16False371.343 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found16False384.100 ns
DictionaryTests<String>Dictionary_TryGetValue_Found16False559.943 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found16False531.615 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound16False149.167 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound16False120.109 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound16False109.596 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound16False91.319 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound16False247.097 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound16False231.592 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound16False356.568 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound16False334.678 ns
DictionaryTests<Guid>Dictionary_Enumerate16False61.120 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate16False27.948 ns
DictionaryTests<Int32>Dictionary_Enumerate16False50.372 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate16False27.162 ns
DictionaryTests<Object>Dictionary_Enumerate16False89.827 ns
DictionaryTests<Object>FrozenDictionary_Enumerate16False74.392 ns
DictionaryTests<String>Dictionary_Enumerate16False89.446 ns
DictionaryTests<String>FrozenDictionary_Enumerate16False74.626 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys16False49.057 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys16False4.847 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys16False37.000 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys16False5.108 ns
DictionaryTests<Object>Dictionary_EnumerateKeys16False74.497 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys16False18.488 ns
DictionaryTests<String>Dictionary_EnumerateKeys16False74.906 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys16False18.571 ns
DictionaryTests<Guid>Dictionary_EnumerateValues16False38.145 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues16False5.054 ns
DictionaryTests<Int32>Dictionary_EnumerateValues16False36.834 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues16False5.103 ns
DictionaryTests<Object>Dictionary_EnumerateValues16False54.844 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues16False5.045 ns
DictionaryTests<String>Dictionary_EnumerateValues16False55.299 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues16False5.048 ns
DictionaryTests<Guid>Dictionary_Construct16True276.233 ns
DictionaryTests<Guid>FrozenDictionary_Construct16True2,480.023 ns
DictionaryTests<Int32>Dictionary_Construct16True247.043 ns
DictionaryTests<Int32>FrozenDictionary_Construct16True2,679.061 ns
DictionaryTests<Object>Dictionary_Construct16True401.734 ns
DictionaryTests<Object>FrozenDictionary_Construct16True2,782.085 ns
DictionaryTests<String>Dictionary_Construct16True360.930 ns
DictionaryTests<String>FrozenDictionary_Construct16True1,385.360 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found16True116.494 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found16True124.232 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found16True87.759 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found16True50.979 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found16True207.784 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found16True209.461 ns
DictionaryTests<String>Dictionary_TryGetValue_Found16True268.939 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found16True142.170 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound16True99.116 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound16True81.180 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound16True72.956 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound16True53.239 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound16True163.937 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound16True144.715 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound16True178.219 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound16True109.456 ns
DictionaryTests<Guid>Dictionary_Enumerate16True58.001 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate16True30.898 ns
DictionaryTests<Int32>Dictionary_Enumerate16True49.878 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate16True27.879 ns
DictionaryTests<Object>Dictionary_Enumerate16True89.847 ns
DictionaryTests<Object>FrozenDictionary_Enumerate16True74.568 ns
DictionaryTests<String>Dictionary_Enumerate16True89.347 ns
DictionaryTests<String>FrozenDictionary_Enumerate16True73.377 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys16True48.112 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys16True5.259 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys16True36.038 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys16True5.016 ns
DictionaryTests<Object>Dictionary_EnumerateKeys16True74.331 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys16True19.002 ns
DictionaryTests<String>Dictionary_EnumerateKeys16True73.520 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys16True18.961 ns
DictionaryTests<Guid>Dictionary_EnumerateValues16True39.389 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues16True5.432 ns
DictionaryTests<Int32>Dictionary_EnumerateValues16True37.072 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues16True4.999 ns
DictionaryTests<Object>Dictionary_EnumerateValues16True54.973 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues16True5.033 ns
DictionaryTests<String>Dictionary_EnumerateValues16True54.496 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues16True5.029 ns
DictionaryTests<Guid>Dictionary_Construct1024False20,251.116 ns
DictionaryTests<Guid>FrozenDictionary_Construct1024False165,973.254 ns
DictionaryTests<Int32>Dictionary_Construct1024False16,858.288 ns
DictionaryTests<Int32>FrozenDictionary_Construct1024False149,391.455 ns
DictionaryTests<Object>Dictionary_Construct1024False26,154.444 ns
DictionaryTests<Object>FrozenDictionary_Construct1024False174,545.846 ns
DictionaryTests<String>Dictionary_Construct1024False36,381.752 ns
DictionaryTests<String>FrozenDictionary_Construct1024False200,478.010 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found1024False16,232.339 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found1024False14,450.573 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found1024False10,903.514 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found1024False9,294.073 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found1024False24,834.107 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found1024False27,807.133 ns
DictionaryTests<String>Dictionary_TryGetValue_Found1024False43,093.660 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found1024False39,181.201 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound1024False13,773.527 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound1024False8,281.616 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound1024False10,252.792 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound1024False6,199.940 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound1024False20,427.130 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound1024False15,131.280 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound1024False26,598.257 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound1024False24,474.515 ns
DictionaryTests<Guid>Dictionary_Enumerate1024False3,440.145 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate1024False1,689.686 ns
DictionaryTests<Int32>Dictionary_Enumerate1024False2,520.821 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate1024False1,498.062 ns
DictionaryTests<Object>Dictionary_Enumerate1024False4,408.578 ns
DictionaryTests<Object>FrozenDictionary_Enumerate1024False3,689.598 ns
DictionaryTests<String>Dictionary_Enumerate1024False4,398.440 ns
DictionaryTests<String>FrozenDictionary_Enumerate1024False3,636.864 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys1024False2,359.947 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys1024False261.473 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys1024False2,033.189 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys1024False253.029 ns
DictionaryTests<Object>Dictionary_EnumerateKeys1024False3,742.122 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys1024False1,226.121 ns
DictionaryTests<String>Dictionary_EnumerateKeys1024False3,693.788 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys1024False1,222.912 ns
DictionaryTests<Guid>Dictionary_EnumerateValues1024False2,144.243 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues1024False260.725 ns
DictionaryTests<Int32>Dictionary_EnumerateValues1024False2,039.401 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues1024False255.137 ns
DictionaryTests<Object>Dictionary_EnumerateValues1024False2,900.210 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues1024False254.610 ns
DictionaryTests<String>Dictionary_EnumerateValues1024False2,924.998 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues1024False252.582 ns
DictionaryTests<Guid>Dictionary_Construct1024True17,653.020 ns
DictionaryTests<Guid>FrozenDictionary_Construct1024True165,900.270 ns
DictionaryTests<Int32>Dictionary_Construct1024True15,324.775 ns
DictionaryTests<Int32>FrozenDictionary_Construct1024True145,562.184 ns
DictionaryTests<Object>Dictionary_Construct1024True22,357.810 ns
DictionaryTests<Object>FrozenDictionary_Construct1024True164,976.669 ns
DictionaryTests<String>Dictionary_Construct1024True24,584.158 ns
DictionaryTests<String>FrozenDictionary_Construct1024True713,506.562 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found1024True10,972.369 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found1024True10,677.337 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found1024True6,061.961 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found1024True4,568.878 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found1024True15,521.277 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found1024True15,039.072 ns
DictionaryTests<String>Dictionary_TryGetValue_Found1024True21,252.949 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found1024True19,273.593 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound1024True10,827.871 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound1024True6,294.780 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound1024True8,168.398 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound1024True4,152.874 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound1024True14,748.903 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound1024True9,459.935 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound1024True20,727.839 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound1024True11,941.794 ns
DictionaryTests<Guid>Dictionary_Enumerate1024True3,355.945 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate1024True1,676.260 ns
DictionaryTests<Int32>Dictionary_Enumerate1024True2,501.281 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate1024True1,488.353 ns
DictionaryTests<Object>Dictionary_Enumerate1024True4,433.532 ns
DictionaryTests<Object>FrozenDictionary_Enumerate1024True3,662.125 ns
DictionaryTests<String>Dictionary_Enumerate1024True4,485.860 ns
DictionaryTests<String>FrozenDictionary_Enumerate1024True3,652.590 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys1024True2,351.717 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys1024True262.712 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys1024True2,019.313 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys1024True252.664 ns
DictionaryTests<Object>Dictionary_EnumerateKeys1024True3,673.043 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys1024True1,224.610 ns
DictionaryTests<String>Dictionary_EnumerateKeys1024True3,748.785 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys1024True1,228.282 ns
DictionaryTests<Guid>Dictionary_EnumerateValues1024True2,080.066 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues1024True255.457 ns
DictionaryTests<Int32>Dictionary_EnumerateValues1024True2,018.673 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues1024True255.488 ns
DictionaryTests<Object>Dictionary_EnumerateValues1024True2,872.265 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues1024True255.872 ns
DictionaryTests<String>Dictionary_EnumerateValues1024True2,895.610 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues1024True254.709 ns
Author:stephentoub
Assignees:-
Labels:

area-System.Collections

Milestone:8.0.0

@ghostghost assigned stephentoubNov 2, 2022
@geeknoid

Copy link
Copy Markdown
Member

The read benchmarks look pretty good, but the construction phase is definitely on the slow side. I never really measure the construction time since for my scenarios I didn't care so much. I do think adding some notion of "intensity" in the constructor to let the caller decide how much time to spend optimizing would be desirable.

It would be interesting in particular to see what the read perf is if the implementation doesn't perform any optimization at all. It would likely still be faster than a normal Dictionary/HashSet when using the default comparers, but not by much. But if that case is still a bit faster and construction time is on par with normal Dictionary/HashSet, then that says that any scenario that needs a readonly dictionary or set should be using the frozen ones by default since it would be faster.

Without the ability to tune how much time is spent constructing, then you have to be careful where you use the frozen collections and make sure the cost/benefit will be worth it.

@geeknoid

geeknoid commented Nov 2, 2022

Copy link
Copy Markdown
Member

Some thoughts for the future:

  • Dictionaries and sets with low cardinality are common. We should look at adding specialized implementation for 1 and 2 entry collections. This saves memory and speeds things up.

  • I had on my todo list to create specialized two character string comparers to optimize that case. For small collections, often the single char comparer is enough and works well. But when the collection grows, you run out of single characters to compare, so the code falls back to two.

  • We discussed in the review that the majority use case for this kind of collection is where the data comes dynamically into the process. This says that doing the optimization work for the collections in a source generator is not likely to be used frequently. However, what I think might be useful is the ability to serialize/deserialize the internal state for the frozen collections in a portable way. This would let me distribute this serialized state as config, and thus forego the construction overhead. So instead of putting a bunch of key/value pairs in my config and having to optimize those in each of my service's processes, I could optimize the key/value pairs offline and push the resulting state to the processes.

stephentouband others added 2 commits November 2, 2022 17:45
…y compile
Co-authored-by: Martin Taillefer <mataille@microsoft.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>

@geeknoidgeeknoid left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This looks great.

@krwq

krwq commented Nov 4, 2022

Copy link
Copy Markdown
Member

I'd be really interested to see how integration of this feature would look like with System.Text.Json (both JsonSerializerOptions.Converters and JsonTypeInfo.Properties are freezing values at one point)

krwq
krwq approved these changes Nov 4, 2022

@krwqkrwq left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I've skimmed all of the files and overall looks good. I'm really impressed in the amount of heuristics done here to make this super optimized. Thank you!

@stephentoub
stephentoub merged commit 6c5a440 into dotnet:mainNov 4, 2022
@stephentoub
stephentoub deleted the frozencollections branch November 4, 2022 19:49
@ghostghost locked as resolved and limited conversation to collaborators Dec 5, 2022
@jeffhandleyjeffhandley added the blog-candidate Completed PRs that are candidate topics for blog post coverage label Mar 14, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Collectionsblog-candidateCompleted PRs that are candidate topics for blog post coveragenew-api-needs-documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[API Proposal]: Provide optimized read-only collections

4 participants

@stephentoub@geeknoid@krwq@jeffhandley
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add FrozenDictionary/Set - #77799

Merged
stephentoub merged 3 commits into
dotnet:mainfrom
stephentoub:frozencollections
Nov 4, 2022
Merged

Add FrozenDictionary/Set#77799
stephentoub merged 3 commits into
dotnet:mainfrom
stephentoub:frozencollections

Conversation

@stephentoub

@stephentoubstephentoub commented Nov 2, 2022

Copy link
Copy Markdown
Member

Fixes#67209

The first commit imports @geeknoid's source, updated to compile in System.Collections.Immutable.dll.

The second commit overhauls the APIs and adds tests. Overhauling the APIs also required lots of surgery on various aspects of the implementation.

This includes a bunch of updates to our shared collections tests, which we've apparently not used in earnest with read-only collections, as a bunch of mutating tests were trying to run even when IsReadOnly was set to false.

There's still more work required here. Subsequent to this PR, we should:

  • Investigate ways to make construction faster.
  • Investigate ways to make lookups faster.
  • Investigate what exact special-cased implementations we want to ship.

(@geeknoid, your help here would be appreciated. Best case numbers look very good, e.g. the example benchmarks you previously created, but others don't look nearly as good. I vetted some of the worst numbers against the original implementation, and they're similar.)

Right now, for each of dictionary/set, there are custom implementations for:

  • Int32 keys with the default comparer
  • Value type keys with the default comparer
  • String keys with default/ordinal/ordinal-ignore-case comparers. Here there are two: one that tries to bucket a small number of strings by length, and one that tries to avoid reading/hashing the full strings by selecting the smallest portion that's unique enough.
  • A general fallback implementation that's used for everything else.

We can explore other data structures, other optimizations, etc., all behind the same veneer. Some of the existing implementations might not be worthwhile, too. For some situations, we might even want an implementation that just wraps Dictionary<,> along with dedicated keys/values arrays.

But the first step is getting the APIs merged to enable others to experiment.

Some benchmarks
[InProcess][GenericTypeArguments(typeof(string))][GenericTypeArguments(typeof(int))][GenericTypeArguments(typeof(object))][GenericTypeArguments(typeof(Guid))]publicclassDictionaryTests<TKey>{privateTKey[]_keys;privateTKey[]_nonExistentKeys;privateDictionary<TKey,long>_dictionary;privateFrozenDictionary<TKey,long>_frozenDictionary;[Params(1,16,1024)]publicintCount{get;set;}[Params(false,true)]publicboolDefaultComparer{get;set;}privatestaticIEqualityComparer<TKey>GetComparer(booldefaultComparer)=>defaultComparer?EqualityComparer<TKey>.Default:NonDefaultEqualityComparer<TKey>.Instance;privateunsafestaticTRandGetRandomValue<TRand>(Randomrand){if(typeof(TRand)==typeof(string)){Span<char>span=stackallocchar[rand.Next(5,20)];for(inti=0;i<span.Length;i++){span[i]=(char)('a'+rand.Next(0,26));}return(TRand)(object)span.ToString();}if(typeof(TRand)==typeof(object)){return(TRand)newobject();}if(typeof(TRand)==typeof(Guid)){Int128value=newInt128((ulong)rand.NextInt64(),(ulong)rand.NextInt64());return(TRand)(object)(*(Guid*)(&value));}if(typeof(TRand)==typeof(int)){return(TRand)(object)rand.Next();}if(typeof(TRand)==typeof(long)){return(TRand)(object)rand.NextInt64();}thrownewException("Unknown type");}[GlobalSetup]publicvoidSetup(){varrand=newRandom(42);_dictionary=newDictionary<TKey,long>(GetComparer(DefaultComparer));for(intj=0;j<Count;j++){_dictionary[GetRandomValue<TKey>(rand)]=GetRandomValue<long>(rand);}_frozenDictionary=_dictionary.ToFrozenDictionary(GetComparer(DefaultComparer));_keys=_dictionary.Keys.ToArray();if(typeof(TKey)==typeof(string)){_keys=(TKey[])(object)Array.ConvertAll((string[])(object)_keys,string.Copy);}_nonExistentKeys=newTKey[Count];for(inti=0;i<Count;i++){TKeykey;while(_dictionary.ContainsKey(key=GetRandomValue<TKey>(rand)));_nonExistentKeys[i]=key;}}[Benchmark]publicDictionary<TKey,long>Dictionary_Construct(){return_keys.ToDictionary((TKeyk)=>k,(TKeyk)=>0L,GetComparer(DefaultComparer));}[Benchmark]publicFrozenDictionary<TKey,long>FrozenDictionary_Construct(){return_keys.ToFrozenDictionary((TKeyk)=>k,(TKeyk)=>0L,GetComparer(DefaultComparer));}[Benchmark]publicboolDictionary_TryGetValue_Found(){boolallFound=true;TKey[]keys=_keys;foreach(TKeykeyinkeys){longvalue;allFound&=_dictionary.TryGetValue(key,outvalue);}returnallFound;}[Benchmark]publicboolDictionary_TryGetValue_NotFound(){boolanyFound=false;TKey[]nonExistentKeys=_nonExistentKeys;foreach(TKeykeyinnonExistentKeys){longvalue;anyFound|=_dictionary.TryGetValue(key,outvalue);}returnanyFound;}[Benchmark]publicboolFrozenDictionary_TryGetValue_Found(){boolallFound=true;TKey[]keys=_keys;foreach(TKeykeyinkeys){longvalue;allFound&=_frozenDictionary.TryGetValue(key,outvalue);}returnallFound;}[Benchmark]publicboolFrozenDictionary_TryGetValue_NotFound(){boolanyFound=false;TKey[]nonExistentKeys=_nonExistentKeys;foreach(TKeykeyinnonExistentKeys){longvalue;anyFound|=_frozenDictionary.TryGetValue(key,outvalue);}returnanyFound;}[Benchmark]publicintDictionary_Enumerate(){intcount=0;foreach(KeyValuePair<TKey,long>itemin_dictionary){_=item;count++;}returncount;}[Benchmark]publicintDictionary_EnumerateKeys(){intcount=0;foreach(TKeykeyin_dictionary.Keys){_=key;count++;}returncount;}[Benchmark]publiclongDictionary_EnumerateValues(){intcount=0;foreach(longvaluein_dictionary.Values){_=value;count++;}returncount;}[Benchmark]publicintFrozenDictionary_Enumerate(){intcount=0;foreach(KeyValuePair<TKey,long>itemin_frozenDictionary){_=item;count++;}returncount;}[Benchmark]publicintFrozenDictionary_EnumerateKeys(){intcount=0;foreach(TKeykeyin_frozenDictionary.Keys){count++;}returncount;}[Benchmark]publicintFrozenDictionary_EnumerateValues(){intcount=0;foreach(longvaluein_frozenDictionary.Values){count++;}returncount;}}internalsealedpartialclassNonDefaultEqualityComparer<T>:EqualityComparer<T>{publicstaticNonDefaultEqualityComparer<T>Instance{get;}=newNonDefaultEqualityComparer<T>();publicoverrideboolEquals(Tx,Ty)=>Default.Equals(x,y);publicoverrideintGetHashCode(Tobj)=>Default.GetHashCode(obj);}
TypeMethodCountDefaultComparerMean
DictionaryTests<Guid>Dictionary_Construct16False308.917 ns
DictionaryTests<Guid>FrozenDictionary_Construct16False2,546.966 ns
DictionaryTests<Int32>Dictionary_Construct16False256.919 ns
DictionaryTests<Int32>FrozenDictionary_Construct16False2,735.513 ns
DictionaryTests<Object>Dictionary_Construct16False459.599 ns
DictionaryTests<Object>FrozenDictionary_Construct16False2,990.372 ns
DictionaryTests<String>Dictionary_Construct16False574.025 ns
DictionaryTests<String>FrozenDictionary_Construct16False3,085.790 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found16False195.613 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found16False180.628 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found16False127.828 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found16False120.595 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found16False371.343 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found16False384.100 ns
DictionaryTests<String>Dictionary_TryGetValue_Found16False559.943 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found16False531.615 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound16False149.167 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound16False120.109 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound16False109.596 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound16False91.319 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound16False247.097 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound16False231.592 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound16False356.568 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound16False334.678 ns
DictionaryTests<Guid>Dictionary_Enumerate16False61.120 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate16False27.948 ns
DictionaryTests<Int32>Dictionary_Enumerate16False50.372 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate16False27.162 ns
DictionaryTests<Object>Dictionary_Enumerate16False89.827 ns
DictionaryTests<Object>FrozenDictionary_Enumerate16False74.392 ns
DictionaryTests<String>Dictionary_Enumerate16False89.446 ns
DictionaryTests<String>FrozenDictionary_Enumerate16False74.626 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys16False49.057 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys16False4.847 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys16False37.000 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys16False5.108 ns
DictionaryTests<Object>Dictionary_EnumerateKeys16False74.497 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys16False18.488 ns
DictionaryTests<String>Dictionary_EnumerateKeys16False74.906 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys16False18.571 ns
DictionaryTests<Guid>Dictionary_EnumerateValues16False38.145 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues16False5.054 ns
DictionaryTests<Int32>Dictionary_EnumerateValues16False36.834 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues16False5.103 ns
DictionaryTests<Object>Dictionary_EnumerateValues16False54.844 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues16False5.045 ns
DictionaryTests<String>Dictionary_EnumerateValues16False55.299 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues16False5.048 ns
DictionaryTests<Guid>Dictionary_Construct16True276.233 ns
DictionaryTests<Guid>FrozenDictionary_Construct16True2,480.023 ns
DictionaryTests<Int32>Dictionary_Construct16True247.043 ns
DictionaryTests<Int32>FrozenDictionary_Construct16True2,679.061 ns
DictionaryTests<Object>Dictionary_Construct16True401.734 ns
DictionaryTests<Object>FrozenDictionary_Construct16True2,782.085 ns
DictionaryTests<String>Dictionary_Construct16True360.930 ns
DictionaryTests<String>FrozenDictionary_Construct16True1,385.360 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found16True116.494 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found16True124.232 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found16True87.759 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found16True50.979 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found16True207.784 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found16True209.461 ns
DictionaryTests<String>Dictionary_TryGetValue_Found16True268.939 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found16True142.170 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound16True99.116 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound16True81.180 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound16True72.956 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound16True53.239 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound16True163.937 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound16True144.715 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound16True178.219 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound16True109.456 ns
DictionaryTests<Guid>Dictionary_Enumerate16True58.001 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate16True30.898 ns
DictionaryTests<Int32>Dictionary_Enumerate16True49.878 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate16True27.879 ns
DictionaryTests<Object>Dictionary_Enumerate16True89.847 ns
DictionaryTests<Object>FrozenDictionary_Enumerate16True74.568 ns
DictionaryTests<String>Dictionary_Enumerate16True89.347 ns
DictionaryTests<String>FrozenDictionary_Enumerate16True73.377 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys16True48.112 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys16True5.259 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys16True36.038 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys16True5.016 ns
DictionaryTests<Object>Dictionary_EnumerateKeys16True74.331 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys16True19.002 ns
DictionaryTests<String>Dictionary_EnumerateKeys16True73.520 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys16True18.961 ns
DictionaryTests<Guid>Dictionary_EnumerateValues16True39.389 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues16True5.432 ns
DictionaryTests<Int32>Dictionary_EnumerateValues16True37.072 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues16True4.999 ns
DictionaryTests<Object>Dictionary_EnumerateValues16True54.973 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues16True5.033 ns
DictionaryTests<String>Dictionary_EnumerateValues16True54.496 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues16True5.029 ns
DictionaryTests<Guid>Dictionary_Construct1024False20,251.116 ns
DictionaryTests<Guid>FrozenDictionary_Construct1024False165,973.254 ns
DictionaryTests<Int32>Dictionary_Construct1024False16,858.288 ns
DictionaryTests<Int32>FrozenDictionary_Construct1024False149,391.455 ns
DictionaryTests<Object>Dictionary_Construct1024False26,154.444 ns
DictionaryTests<Object>FrozenDictionary_Construct1024False174,545.846 ns
DictionaryTests<String>Dictionary_Construct1024False36,381.752 ns
DictionaryTests<String>FrozenDictionary_Construct1024False200,478.010 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found1024False16,232.339 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found1024False14,450.573 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found1024False10,903.514 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found1024False9,294.073 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found1024False24,834.107 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found1024False27,807.133 ns
DictionaryTests<String>Dictionary_TryGetValue_Found1024False43,093.660 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found1024False39,181.201 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound1024False13,773.527 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound1024False8,281.616 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound1024False10,252.792 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound1024False6,199.940 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound1024False20,427.130 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound1024False15,131.280 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound1024False26,598.257 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound1024False24,474.515 ns
DictionaryTests<Guid>Dictionary_Enumerate1024False3,440.145 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate1024False1,689.686 ns
DictionaryTests<Int32>Dictionary_Enumerate1024False2,520.821 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate1024False1,498.062 ns
DictionaryTests<Object>Dictionary_Enumerate1024False4,408.578 ns
DictionaryTests<Object>FrozenDictionary_Enumerate1024False3,689.598 ns
DictionaryTests<String>Dictionary_Enumerate1024False4,398.440 ns
DictionaryTests<String>FrozenDictionary_Enumerate1024False3,636.864 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys1024False2,359.947 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys1024False261.473 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys1024False2,033.189 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys1024False253.029 ns
DictionaryTests<Object>Dictionary_EnumerateKeys1024False3,742.122 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys1024False1,226.121 ns
DictionaryTests<String>Dictionary_EnumerateKeys1024False3,693.788 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys1024False1,222.912 ns
DictionaryTests<Guid>Dictionary_EnumerateValues1024False2,144.243 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues1024False260.725 ns
DictionaryTests<Int32>Dictionary_EnumerateValues1024False2,039.401 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues1024False255.137 ns
DictionaryTests<Object>Dictionary_EnumerateValues1024False2,900.210 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues1024False254.610 ns
DictionaryTests<String>Dictionary_EnumerateValues1024False2,924.998 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues1024False252.582 ns
DictionaryTests<Guid>Dictionary_Construct1024True17,653.020 ns
DictionaryTests<Guid>FrozenDictionary_Construct1024True165,900.270 ns
DictionaryTests<Int32>Dictionary_Construct1024True15,324.775 ns
DictionaryTests<Int32>FrozenDictionary_Construct1024True145,562.184 ns
DictionaryTests<Object>Dictionary_Construct1024True22,357.810 ns
DictionaryTests<Object>FrozenDictionary_Construct1024True164,976.669 ns
DictionaryTests<String>Dictionary_Construct1024True24,584.158 ns
DictionaryTests<String>FrozenDictionary_Construct1024True713,506.562 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found1024True10,972.369 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found1024True10,677.337 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found1024True6,061.961 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found1024True4,568.878 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found1024True15,521.277 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found1024True15,039.072 ns
DictionaryTests<String>Dictionary_TryGetValue_Found1024True21,252.949 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found1024True19,273.593 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound1024True10,827.871 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound1024True6,294.780 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound1024True8,168.398 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound1024True4,152.874 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound1024True14,748.903 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound1024True9,459.935 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound1024True20,727.839 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound1024True11,941.794 ns
DictionaryTests<Guid>Dictionary_Enumerate1024True3,355.945 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate1024True1,676.260 ns
DictionaryTests<Int32>Dictionary_Enumerate1024True2,501.281 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate1024True1,488.353 ns
DictionaryTests<Object>Dictionary_Enumerate1024True4,433.532 ns
DictionaryTests<Object>FrozenDictionary_Enumerate1024True3,662.125 ns
DictionaryTests<String>Dictionary_Enumerate1024True4,485.860 ns
DictionaryTests<String>FrozenDictionary_Enumerate1024True3,652.590 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys1024True2,351.717 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys1024True262.712 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys1024True2,019.313 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys1024True252.664 ns
DictionaryTests<Object>Dictionary_EnumerateKeys1024True3,673.043 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys1024True1,224.610 ns
DictionaryTests<String>Dictionary_EnumerateKeys1024True3,748.785 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys1024True1,228.282 ns
DictionaryTests<Guid>Dictionary_EnumerateValues1024True2,080.066 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues1024True255.457 ns
DictionaryTests<Int32>Dictionary_EnumerateValues1024True2,018.673 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues1024True255.488 ns
DictionaryTests<Object>Dictionary_EnumerateValues1024True2,872.265 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues1024True255.872 ns
DictionaryTests<String>Dictionary_EnumerateValues1024True2,895.610 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues1024True254.709 ns

@ghost

ghost commented Nov 2, 2022

Copy link
Copy Markdown

Note regarding the new-api-needs-documentation label:

This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, to please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change.

@ghost

ghost commented Nov 2, 2022

Copy link
Copy Markdown

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

Issue Details

The first commit imports @geeknoid's source, updated to compile in System.Collections.Immutable.dll.

The second commit overhauls the APIs and adds tests.

This includes a bunch of updates to our shared collections tests, which we've apparently not used in earnest with read-only collections, as a bunch of mutating tests were trying to run even when IsReadOnly was set to false.

There's still more work required here. Subsequent to this PR, we should:

  • Investigate ways to make construction faster.
  • Investigate ways to make lookups faster.
  • Investigate what exact special-cased implementations we want to ship.

(@geeknoid, your help here would be appreciated. Best case numbers look very good, e.g. the example benchmarks you previously created, but others don't look nearly as good. I vetted some of the worst numbers against the original implementation, and they're similar.)

Right now, for each of dictionary/set, there are custom implementations for:

  • Int32 keys with the default comparer
  • Value type keys with the default comparer
  • String keys with default/ordinal/ordinal-ignore-case comparers. Here there are two: one that tries to bucket a small number of strings by length, and one that tries to avoid reading/hashing the full strings by selecting the smallest portion that's unique enough.
  • A general fallback implementation that's used for everything else.

We can explore other data structures, other optimizations, etc., all behind the same veneer. Some of the existing implementations might not be worthwhile, too. For some situations, we might even want an implementation that just wraps Dictionary<,> along with dedicated keys/values arrays.

But the first step is getting the APIs merged to enable others to experiment.

Some benchmarks
[InProcess][GenericTypeArguments(typeof(string))][GenericTypeArguments(typeof(int))][GenericTypeArguments(typeof(object))][GenericTypeArguments(typeof(Guid))]publicclassDictionaryTests<TKey>{privateTKey[]_keys;privateTKey[]_nonExistentKeys;privateDictionary<TKey,long>_dictionary;privateFrozenDictionary<TKey,long>_frozenDictionary;[Params(1,16,1024)]publicintCount{get;set;}[Params(false,true)]publicboolDefaultComparer{get;set;}privatestaticIEqualityComparer<TKey>GetComparer(booldefaultComparer)=>defaultComparer?EqualityComparer<TKey>.Default:NonDefaultEqualityComparer<TKey>.Instance;privateunsafestaticTRandGetRandomValue<TRand>(Randomrand){if(typeof(TRand)==typeof(string)){Span<char>span=stackallocchar[rand.Next(5,20)];for(inti=0;i<span.Length;i++){span[i]=(char)('a'+rand.Next(0,26));}return(TRand)(object)span.ToString();}if(typeof(TRand)==typeof(object)){return(TRand)newobject();}if(typeof(TRand)==typeof(Guid)){Int128value=newInt128((ulong)rand.NextInt64(),(ulong)rand.NextInt64());return(TRand)(object)(*(Guid*)(&value));}if(typeof(TRand)==typeof(int)){return(TRand)(object)rand.Next();}if(typeof(TRand)==typeof(long)){return(TRand)(object)rand.NextInt64();}thrownewException("Unknown type");}[GlobalSetup]publicvoidSetup(){varrand=newRandom(42);_dictionary=newDictionary<TKey,long>(GetComparer(DefaultComparer));for(intj=0;j<Count;j++){_dictionary[GetRandomValue<TKey>(rand)]=GetRandomValue<long>(rand);}_frozenDictionary=_dictionary.ToFrozenDictionary(GetComparer(DefaultComparer));_keys=_dictionary.Keys.ToArray();if(typeof(TKey)==typeof(string)){_keys=(TKey[])(object)Array.ConvertAll((string[])(object)_keys,string.Copy);}_nonExistentKeys=newTKey[Count];for(inti=0;i<Count;i++){TKeykey;while(_dictionary.ContainsKey(key=GetRandomValue<TKey>(rand)));_nonExistentKeys[i]=key;}}[Benchmark]publicDictionary<TKey,long>Dictionary_Construct(){return_keys.ToDictionary((TKeyk)=>k,(TKeyk)=>0L,GetComparer(DefaultComparer));}[Benchmark]publicFrozenDictionary<TKey,long>FrozenDictionary_Construct(){return_keys.ToFrozenDictionary((TKeyk)=>k,(TKeyk)=>0L,GetComparer(DefaultComparer));}[Benchmark]publicboolDictionary_TryGetValue_Found(){boolallFound=true;TKey[]keys=_keys;foreach(TKeykeyinkeys){longvalue;allFound&=_dictionary.TryGetValue(key,outvalue);}returnallFound;}[Benchmark]publicboolDictionary_TryGetValue_NotFound(){boolanyFound=false;TKey[]nonExistentKeys=_nonExistentKeys;foreach(TKeykeyinnonExistentKeys){longvalue;anyFound|=_dictionary.TryGetValue(key,outvalue);}returnanyFound;}[Benchmark]publicboolFrozenDictionary_TryGetValue_Found(){boolallFound=true;TKey[]keys=_keys;foreach(TKeykeyinkeys){longvalue;allFound&=_frozenDictionary.TryGetValue(key,outvalue);}returnallFound;}[Benchmark]publicboolFrozenDictionary_TryGetValue_NotFound(){boolanyFound=false;TKey[]nonExistentKeys=_nonExistentKeys;foreach(TKeykeyinnonExistentKeys){longvalue;anyFound|=_frozenDictionary.TryGetValue(key,outvalue);}returnanyFound;}[Benchmark]publicintDictionary_Enumerate(){intcount=0;foreach(KeyValuePair<TKey,long>itemin_dictionary){_=item;count++;}returncount;}[Benchmark]publicintDictionary_EnumerateKeys(){intcount=0;foreach(TKeykeyin_dictionary.Keys){_=key;count++;}returncount;}[Benchmark]publiclongDictionary_EnumerateValues(){intcount=0;foreach(longvaluein_dictionary.Values){_=value;count++;}returncount;}[Benchmark]publicintFrozenDictionary_Enumerate(){intcount=0;foreach(KeyValuePair<TKey,long>itemin_frozenDictionary){_=item;count++;}returncount;}[Benchmark]publicintFrozenDictionary_EnumerateKeys(){intcount=0;foreach(TKeykeyin_frozenDictionary.Keys){count++;}returncount;}[Benchmark]publicintFrozenDictionary_EnumerateValues(){intcount=0;foreach(longvaluein_frozenDictionary.Values){count++;}returncount;}}internalsealedpartialclassNonDefaultEqualityComparer<T>:EqualityComparer<T>{publicstaticNonDefaultEqualityComparer<T>Instance{get;}=newNonDefaultEqualityComparer<T>();publicoverrideboolEquals(Tx,Ty)=>Default.Equals(x,y);publicoverrideintGetHashCode(Tobj)=>Default.GetHashCode(obj);}
TypeMethodCountDefaultComparerMean
DictionaryTests<Guid>Dictionary_Construct16False308.917 ns
DictionaryTests<Guid>FrozenDictionary_Construct16False2,546.966 ns
DictionaryTests<Int32>Dictionary_Construct16False256.919 ns
DictionaryTests<Int32>FrozenDictionary_Construct16False2,735.513 ns
DictionaryTests<Object>Dictionary_Construct16False459.599 ns
DictionaryTests<Object>FrozenDictionary_Construct16False2,990.372 ns
DictionaryTests<String>Dictionary_Construct16False574.025 ns
DictionaryTests<String>FrozenDictionary_Construct16False3,085.790 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found16False195.613 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found16False180.628 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found16False127.828 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found16False120.595 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found16False371.343 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found16False384.100 ns
DictionaryTests<String>Dictionary_TryGetValue_Found16False559.943 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found16False531.615 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound16False149.167 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound16False120.109 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound16False109.596 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound16False91.319 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound16False247.097 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound16False231.592 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound16False356.568 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound16False334.678 ns
DictionaryTests<Guid>Dictionary_Enumerate16False61.120 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate16False27.948 ns
DictionaryTests<Int32>Dictionary_Enumerate16False50.372 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate16False27.162 ns
DictionaryTests<Object>Dictionary_Enumerate16False89.827 ns
DictionaryTests<Object>FrozenDictionary_Enumerate16False74.392 ns
DictionaryTests<String>Dictionary_Enumerate16False89.446 ns
DictionaryTests<String>FrozenDictionary_Enumerate16False74.626 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys16False49.057 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys16False4.847 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys16False37.000 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys16False5.108 ns
DictionaryTests<Object>Dictionary_EnumerateKeys16False74.497 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys16False18.488 ns
DictionaryTests<String>Dictionary_EnumerateKeys16False74.906 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys16False18.571 ns
DictionaryTests<Guid>Dictionary_EnumerateValues16False38.145 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues16False5.054 ns
DictionaryTests<Int32>Dictionary_EnumerateValues16False36.834 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues16False5.103 ns
DictionaryTests<Object>Dictionary_EnumerateValues16False54.844 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues16False5.045 ns
DictionaryTests<String>Dictionary_EnumerateValues16False55.299 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues16False5.048 ns
DictionaryTests<Guid>Dictionary_Construct16True276.233 ns
DictionaryTests<Guid>FrozenDictionary_Construct16True2,480.023 ns
DictionaryTests<Int32>Dictionary_Construct16True247.043 ns
DictionaryTests<Int32>FrozenDictionary_Construct16True2,679.061 ns
DictionaryTests<Object>Dictionary_Construct16True401.734 ns
DictionaryTests<Object>FrozenDictionary_Construct16True2,782.085 ns
DictionaryTests<String>Dictionary_Construct16True360.930 ns
DictionaryTests<String>FrozenDictionary_Construct16True1,385.360 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found16True116.494 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found16True124.232 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found16True87.759 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found16True50.979 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found16True207.784 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found16True209.461 ns
DictionaryTests<String>Dictionary_TryGetValue_Found16True268.939 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found16True142.170 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound16True99.116 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound16True81.180 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound16True72.956 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound16True53.239 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound16True163.937 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound16True144.715 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound16True178.219 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound16True109.456 ns
DictionaryTests<Guid>Dictionary_Enumerate16True58.001 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate16True30.898 ns
DictionaryTests<Int32>Dictionary_Enumerate16True49.878 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate16True27.879 ns
DictionaryTests<Object>Dictionary_Enumerate16True89.847 ns
DictionaryTests<Object>FrozenDictionary_Enumerate16True74.568 ns
DictionaryTests<String>Dictionary_Enumerate16True89.347 ns
DictionaryTests<String>FrozenDictionary_Enumerate16True73.377 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys16True48.112 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys16True5.259 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys16True36.038 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys16True5.016 ns
DictionaryTests<Object>Dictionary_EnumerateKeys16True74.331 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys16True19.002 ns
DictionaryTests<String>Dictionary_EnumerateKeys16True73.520 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys16True18.961 ns
DictionaryTests<Guid>Dictionary_EnumerateValues16True39.389 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues16True5.432 ns
DictionaryTests<Int32>Dictionary_EnumerateValues16True37.072 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues16True4.999 ns
DictionaryTests<Object>Dictionary_EnumerateValues16True54.973 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues16True5.033 ns
DictionaryTests<String>Dictionary_EnumerateValues16True54.496 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues16True5.029 ns
DictionaryTests<Guid>Dictionary_Construct1024False20,251.116 ns
DictionaryTests<Guid>FrozenDictionary_Construct1024False165,973.254 ns
DictionaryTests<Int32>Dictionary_Construct1024False16,858.288 ns
DictionaryTests<Int32>FrozenDictionary_Construct1024False149,391.455 ns
DictionaryTests<Object>Dictionary_Construct1024False26,154.444 ns
DictionaryTests<Object>FrozenDictionary_Construct1024False174,545.846 ns
DictionaryTests<String>Dictionary_Construct1024False36,381.752 ns
DictionaryTests<String>FrozenDictionary_Construct1024False200,478.010 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found1024False16,232.339 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found1024False14,450.573 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found1024False10,903.514 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found1024False9,294.073 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found1024False24,834.107 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found1024False27,807.133 ns
DictionaryTests<String>Dictionary_TryGetValue_Found1024False43,093.660 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found1024False39,181.201 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound1024False13,773.527 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound1024False8,281.616 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound1024False10,252.792 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound1024False6,199.940 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound1024False20,427.130 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound1024False15,131.280 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound1024False26,598.257 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound1024False24,474.515 ns
DictionaryTests<Guid>Dictionary_Enumerate1024False3,440.145 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate1024False1,689.686 ns
DictionaryTests<Int32>Dictionary_Enumerate1024False2,520.821 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate1024False1,498.062 ns
DictionaryTests<Object>Dictionary_Enumerate1024False4,408.578 ns
DictionaryTests<Object>FrozenDictionary_Enumerate1024False3,689.598 ns
DictionaryTests<String>Dictionary_Enumerate1024False4,398.440 ns
DictionaryTests<String>FrozenDictionary_Enumerate1024False3,636.864 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys1024False2,359.947 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys1024False261.473 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys1024False2,033.189 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys1024False253.029 ns
DictionaryTests<Object>Dictionary_EnumerateKeys1024False3,742.122 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys1024False1,226.121 ns
DictionaryTests<String>Dictionary_EnumerateKeys1024False3,693.788 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys1024False1,222.912 ns
DictionaryTests<Guid>Dictionary_EnumerateValues1024False2,144.243 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues1024False260.725 ns
DictionaryTests<Int32>Dictionary_EnumerateValues1024False2,039.401 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues1024False255.137 ns
DictionaryTests<Object>Dictionary_EnumerateValues1024False2,900.210 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues1024False254.610 ns
DictionaryTests<String>Dictionary_EnumerateValues1024False2,924.998 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues1024False252.582 ns
DictionaryTests<Guid>Dictionary_Construct1024True17,653.020 ns
DictionaryTests<Guid>FrozenDictionary_Construct1024True165,900.270 ns
DictionaryTests<Int32>Dictionary_Construct1024True15,324.775 ns
DictionaryTests<Int32>FrozenDictionary_Construct1024True145,562.184 ns
DictionaryTests<Object>Dictionary_Construct1024True22,357.810 ns
DictionaryTests<Object>FrozenDictionary_Construct1024True164,976.669 ns
DictionaryTests<String>Dictionary_Construct1024True24,584.158 ns
DictionaryTests<String>FrozenDictionary_Construct1024True713,506.562 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found1024True10,972.369 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found1024True10,677.337 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found1024True6,061.961 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found1024True4,568.878 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found1024True15,521.277 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found1024True15,039.072 ns
DictionaryTests<String>Dictionary_TryGetValue_Found1024True21,252.949 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found1024True19,273.593 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound1024True10,827.871 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound1024True6,294.780 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound1024True8,168.398 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound1024True4,152.874 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound1024True14,748.903 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound1024True9,459.935 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound1024True20,727.839 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound1024True11,941.794 ns
DictionaryTests<Guid>Dictionary_Enumerate1024True3,355.945 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate1024True1,676.260 ns
DictionaryTests<Int32>Dictionary_Enumerate1024True2,501.281 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate1024True1,488.353 ns
DictionaryTests<Object>Dictionary_Enumerate1024True4,433.532 ns
DictionaryTests<Object>FrozenDictionary_Enumerate1024True3,662.125 ns
DictionaryTests<String>Dictionary_Enumerate1024True4,485.860 ns
DictionaryTests<String>FrozenDictionary_Enumerate1024True3,652.590 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys1024True2,351.717 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys1024True262.712 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys1024True2,019.313 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys1024True252.664 ns
DictionaryTests<Object>Dictionary_EnumerateKeys1024True3,673.043 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys1024True1,224.610 ns
DictionaryTests<String>Dictionary_EnumerateKeys1024True3,748.785 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys1024True1,228.282 ns
DictionaryTests<Guid>Dictionary_EnumerateValues1024True2,080.066 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues1024True255.457 ns
DictionaryTests<Int32>Dictionary_EnumerateValues1024True2,018.673 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues1024True255.488 ns
DictionaryTests<Object>Dictionary_EnumerateValues1024True2,872.265 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues1024True255.872 ns
DictionaryTests<String>Dictionary_EnumerateValues1024True2,895.610 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues1024True254.709 ns
Author:stephentoub
Assignees:-
Labels:

area-System.Collections

Milestone:8.0.0

@ghostghost assigned stephentoubNov 2, 2022
@geeknoid

Copy link
Copy Markdown
Member

The read benchmarks look pretty good, but the construction phase is definitely on the slow side. I never really measure the construction time since for my scenarios I didn't care so much. I do think adding some notion of "intensity" in the constructor to let the caller decide how much time to spend optimizing would be desirable.

It would be interesting in particular to see what the read perf is if the implementation doesn't perform any optimization at all. It would likely still be faster than a normal Dictionary/HashSet when using the default comparers, but not by much. But if that case is still a bit faster and construction time is on par with normal Dictionary/HashSet, then that says that any scenario that needs a readonly dictionary or set should be using the frozen ones by default since it would be faster.

Without the ability to tune how much time is spent constructing, then you have to be careful where you use the frozen collections and make sure the cost/benefit will be worth it.

@geeknoid

geeknoid commented Nov 2, 2022

Copy link
Copy Markdown
Member

Some thoughts for the future:

  • Dictionaries and sets with low cardinality are common. We should look at adding specialized implementation for 1 and 2 entry collections. This saves memory and speeds things up.

  • I had on my todo list to create specialized two character string comparers to optimize that case. For small collections, often the single char comparer is enough and works well. But when the collection grows, you run out of single characters to compare, so the code falls back to two.

  • We discussed in the review that the majority use case for this kind of collection is where the data comes dynamically into the process. This says that doing the optimization work for the collections in a source generator is not likely to be used frequently. However, what I think might be useful is the ability to serialize/deserialize the internal state for the frozen collections in a portable way. This would let me distribute this serialized state as config, and thus forego the construction overhead. So instead of putting a bunch of key/value pairs in my config and having to optimize those in each of my service's processes, I could optimize the key/value pairs offline and push the resulting state to the processes.

stephentouband others added 2 commits November 2, 2022 17:45
…y compile
Co-authored-by: Martin Taillefer <mataille@microsoft.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>

@geeknoidgeeknoid left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This looks great.

@krwq

krwq commented Nov 4, 2022

Copy link
Copy Markdown
Member

I'd be really interested to see how integration of this feature would look like with System.Text.Json (both JsonSerializerOptions.Converters and JsonTypeInfo.Properties are freezing values at one point)

krwq
krwq approved these changes Nov 4, 2022

@krwqkrwq left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I've skimmed all of the files and overall looks good. I'm really impressed in the amount of heuristics done here to make this super optimized. Thank you!

@stephentoub
stephentoub merged commit 6c5a440 into dotnet:mainNov 4, 2022
@stephentoub
stephentoub deleted the frozencollections branch November 4, 2022 19:49
@ghostghost locked as resolved and limited conversation to collaborators Dec 5, 2022
@jeffhandleyjeffhandley added the blog-candidate Completed PRs that are candidate topics for blog post coverage label Mar 14, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Collectionsblog-candidateCompleted PRs that are candidate topics for blog post coveragenew-api-needs-documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[API Proposal]: Provide optimized read-only collections

4 participants

@stephentoub@geeknoid@krwq@jeffhandley
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Add FrozenDictionary/Set - #77799

Merged
stephentoub merged 3 commits into
dotnet:mainfrom
stephentoub:frozencollections
Nov 4, 2022
Merged

Add FrozenDictionary/Set#77799
stephentoub merged 3 commits into
dotnet:mainfrom
stephentoub:frozencollections

Conversation

@stephentoub

@stephentoubstephentoub commented Nov 2, 2022

Copy link
Copy Markdown
Member

Fixes#67209

The first commit imports @geeknoid's source, updated to compile in System.Collections.Immutable.dll.

The second commit overhauls the APIs and adds tests. Overhauling the APIs also required lots of surgery on various aspects of the implementation.

This includes a bunch of updates to our shared collections tests, which we've apparently not used in earnest with read-only collections, as a bunch of mutating tests were trying to run even when IsReadOnly was set to false.

There's still more work required here. Subsequent to this PR, we should:

  • Investigate ways to make construction faster.
  • Investigate ways to make lookups faster.
  • Investigate what exact special-cased implementations we want to ship.

(@geeknoid, your help here would be appreciated. Best case numbers look very good, e.g. the example benchmarks you previously created, but others don't look nearly as good. I vetted some of the worst numbers against the original implementation, and they're similar.)

Right now, for each of dictionary/set, there are custom implementations for:

  • Int32 keys with the default comparer
  • Value type keys with the default comparer
  • String keys with default/ordinal/ordinal-ignore-case comparers. Here there are two: one that tries to bucket a small number of strings by length, and one that tries to avoid reading/hashing the full strings by selecting the smallest portion that's unique enough.
  • A general fallback implementation that's used for everything else.

We can explore other data structures, other optimizations, etc., all behind the same veneer. Some of the existing implementations might not be worthwhile, too. For some situations, we might even want an implementation that just wraps Dictionary<,> along with dedicated keys/values arrays.

But the first step is getting the APIs merged to enable others to experiment.

Some benchmarks
[InProcess][GenericTypeArguments(typeof(string))][GenericTypeArguments(typeof(int))][GenericTypeArguments(typeof(object))][GenericTypeArguments(typeof(Guid))]publicclassDictionaryTests<TKey>{privateTKey[]_keys;privateTKey[]_nonExistentKeys;privateDictionary<TKey,long>_dictionary;privateFrozenDictionary<TKey,long>_frozenDictionary;[Params(1,16,1024)]publicintCount{get;set;}[Params(false,true)]publicboolDefaultComparer{get;set;}privatestaticIEqualityComparer<TKey>GetComparer(booldefaultComparer)=>defaultComparer?EqualityComparer<TKey>.Default:NonDefaultEqualityComparer<TKey>.Instance;privateunsafestaticTRandGetRandomValue<TRand>(Randomrand){if(typeof(TRand)==typeof(string)){Span<char>span=stackallocchar[rand.Next(5,20)];for(inti=0;i<span.Length;i++){span[i]=(char)('a'+rand.Next(0,26));}return(TRand)(object)span.ToString();}if(typeof(TRand)==typeof(object)){return(TRand)newobject();}if(typeof(TRand)==typeof(Guid)){Int128value=newInt128((ulong)rand.NextInt64(),(ulong)rand.NextInt64());return(TRand)(object)(*(Guid*)(&value));}if(typeof(TRand)==typeof(int)){return(TRand)(object)rand.Next();}if(typeof(TRand)==typeof(long)){return(TRand)(object)rand.NextInt64();}thrownewException("Unknown type");}[GlobalSetup]publicvoidSetup(){varrand=newRandom(42);_dictionary=newDictionary<TKey,long>(GetComparer(DefaultComparer));for(intj=0;j<Count;j++){_dictionary[GetRandomValue<TKey>(rand)]=GetRandomValue<long>(rand);}_frozenDictionary=_dictionary.ToFrozenDictionary(GetComparer(DefaultComparer));_keys=_dictionary.Keys.ToArray();if(typeof(TKey)==typeof(string)){_keys=(TKey[])(object)Array.ConvertAll((string[])(object)_keys,string.Copy);}_nonExistentKeys=newTKey[Count];for(inti=0;i<Count;i++){TKeykey;while(_dictionary.ContainsKey(key=GetRandomValue<TKey>(rand)));_nonExistentKeys[i]=key;}}[Benchmark]publicDictionary<TKey,long>Dictionary_Construct(){return_keys.ToDictionary((TKeyk)=>k,(TKeyk)=>0L,GetComparer(DefaultComparer));}[Benchmark]publicFrozenDictionary<TKey,long>FrozenDictionary_Construct(){return_keys.ToFrozenDictionary((TKeyk)=>k,(TKeyk)=>0L,GetComparer(DefaultComparer));}[Benchmark]publicboolDictionary_TryGetValue_Found(){boolallFound=true;TKey[]keys=_keys;foreach(TKeykeyinkeys){longvalue;allFound&=_dictionary.TryGetValue(key,outvalue);}returnallFound;}[Benchmark]publicboolDictionary_TryGetValue_NotFound(){boolanyFound=false;TKey[]nonExistentKeys=_nonExistentKeys;foreach(TKeykeyinnonExistentKeys){longvalue;anyFound|=_dictionary.TryGetValue(key,outvalue);}returnanyFound;}[Benchmark]publicboolFrozenDictionary_TryGetValue_Found(){boolallFound=true;TKey[]keys=_keys;foreach(TKeykeyinkeys){longvalue;allFound&=_frozenDictionary.TryGetValue(key,outvalue);}returnallFound;}[Benchmark]publicboolFrozenDictionary_TryGetValue_NotFound(){boolanyFound=false;TKey[]nonExistentKeys=_nonExistentKeys;foreach(TKeykeyinnonExistentKeys){longvalue;anyFound|=_frozenDictionary.TryGetValue(key,outvalue);}returnanyFound;}[Benchmark]publicintDictionary_Enumerate(){intcount=0;foreach(KeyValuePair<TKey,long>itemin_dictionary){_=item;count++;}returncount;}[Benchmark]publicintDictionary_EnumerateKeys(){intcount=0;foreach(TKeykeyin_dictionary.Keys){_=key;count++;}returncount;}[Benchmark]publiclongDictionary_EnumerateValues(){intcount=0;foreach(longvaluein_dictionary.Values){_=value;count++;}returncount;}[Benchmark]publicintFrozenDictionary_Enumerate(){intcount=0;foreach(KeyValuePair<TKey,long>itemin_frozenDictionary){_=item;count++;}returncount;}[Benchmark]publicintFrozenDictionary_EnumerateKeys(){intcount=0;foreach(TKeykeyin_frozenDictionary.Keys){count++;}returncount;}[Benchmark]publicintFrozenDictionary_EnumerateValues(){intcount=0;foreach(longvaluein_frozenDictionary.Values){count++;}returncount;}}internalsealedpartialclassNonDefaultEqualityComparer<T>:EqualityComparer<T>{publicstaticNonDefaultEqualityComparer<T>Instance{get;}=newNonDefaultEqualityComparer<T>();publicoverrideboolEquals(Tx,Ty)=>Default.Equals(x,y);publicoverrideintGetHashCode(Tobj)=>Default.GetHashCode(obj);}
TypeMethodCountDefaultComparerMean
DictionaryTests<Guid>Dictionary_Construct16False308.917 ns
DictionaryTests<Guid>FrozenDictionary_Construct16False2,546.966 ns
DictionaryTests<Int32>Dictionary_Construct16False256.919 ns
DictionaryTests<Int32>FrozenDictionary_Construct16False2,735.513 ns
DictionaryTests<Object>Dictionary_Construct16False459.599 ns
DictionaryTests<Object>FrozenDictionary_Construct16False2,990.372 ns
DictionaryTests<String>Dictionary_Construct16False574.025 ns
DictionaryTests<String>FrozenDictionary_Construct16False3,085.790 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found16False195.613 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found16False180.628 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found16False127.828 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found16False120.595 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found16False371.343 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found16False384.100 ns
DictionaryTests<String>Dictionary_TryGetValue_Found16False559.943 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found16False531.615 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound16False149.167 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound16False120.109 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound16False109.596 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound16False91.319 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound16False247.097 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound16False231.592 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound16False356.568 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound16False334.678 ns
DictionaryTests<Guid>Dictionary_Enumerate16False61.120 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate16False27.948 ns
DictionaryTests<Int32>Dictionary_Enumerate16False50.372 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate16False27.162 ns
DictionaryTests<Object>Dictionary_Enumerate16False89.827 ns
DictionaryTests<Object>FrozenDictionary_Enumerate16False74.392 ns
DictionaryTests<String>Dictionary_Enumerate16False89.446 ns
DictionaryTests<String>FrozenDictionary_Enumerate16False74.626 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys16False49.057 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys16False4.847 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys16False37.000 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys16False5.108 ns
DictionaryTests<Object>Dictionary_EnumerateKeys16False74.497 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys16False18.488 ns
DictionaryTests<String>Dictionary_EnumerateKeys16False74.906 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys16False18.571 ns
DictionaryTests<Guid>Dictionary_EnumerateValues16False38.145 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues16False5.054 ns
DictionaryTests<Int32>Dictionary_EnumerateValues16False36.834 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues16False5.103 ns
DictionaryTests<Object>Dictionary_EnumerateValues16False54.844 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues16False5.045 ns
DictionaryTests<String>Dictionary_EnumerateValues16False55.299 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues16False5.048 ns
DictionaryTests<Guid>Dictionary_Construct16True276.233 ns
DictionaryTests<Guid>FrozenDictionary_Construct16True2,480.023 ns
DictionaryTests<Int32>Dictionary_Construct16True247.043 ns
DictionaryTests<Int32>FrozenDictionary_Construct16True2,679.061 ns
DictionaryTests<Object>Dictionary_Construct16True401.734 ns
DictionaryTests<Object>FrozenDictionary_Construct16True2,782.085 ns
DictionaryTests<String>Dictionary_Construct16True360.930 ns
DictionaryTests<String>FrozenDictionary_Construct16True1,385.360 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found16True116.494 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found16True124.232 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found16True87.759 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found16True50.979 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found16True207.784 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found16True209.461 ns
DictionaryTests<String>Dictionary_TryGetValue_Found16True268.939 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found16True142.170 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound16True99.116 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound16True81.180 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound16True72.956 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound16True53.239 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound16True163.937 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound16True144.715 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound16True178.219 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound16True109.456 ns
DictionaryTests<Guid>Dictionary_Enumerate16True58.001 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate16True30.898 ns
DictionaryTests<Int32>Dictionary_Enumerate16True49.878 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate16True27.879 ns
DictionaryTests<Object>Dictionary_Enumerate16True89.847 ns
DictionaryTests<Object>FrozenDictionary_Enumerate16True74.568 ns
DictionaryTests<String>Dictionary_Enumerate16True89.347 ns
DictionaryTests<String>FrozenDictionary_Enumerate16True73.377 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys16True48.112 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys16True5.259 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys16True36.038 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys16True5.016 ns
DictionaryTests<Object>Dictionary_EnumerateKeys16True74.331 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys16True19.002 ns
DictionaryTests<String>Dictionary_EnumerateKeys16True73.520 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys16True18.961 ns
DictionaryTests<Guid>Dictionary_EnumerateValues16True39.389 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues16True5.432 ns
DictionaryTests<Int32>Dictionary_EnumerateValues16True37.072 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues16True4.999 ns
DictionaryTests<Object>Dictionary_EnumerateValues16True54.973 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues16True5.033 ns
DictionaryTests<String>Dictionary_EnumerateValues16True54.496 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues16True5.029 ns
DictionaryTests<Guid>Dictionary_Construct1024False20,251.116 ns
DictionaryTests<Guid>FrozenDictionary_Construct1024False165,973.254 ns
DictionaryTests<Int32>Dictionary_Construct1024False16,858.288 ns
DictionaryTests<Int32>FrozenDictionary_Construct1024False149,391.455 ns
DictionaryTests<Object>Dictionary_Construct1024False26,154.444 ns
DictionaryTests<Object>FrozenDictionary_Construct1024False174,545.846 ns
DictionaryTests<String>Dictionary_Construct1024False36,381.752 ns
DictionaryTests<String>FrozenDictionary_Construct1024False200,478.010 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found1024False16,232.339 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found1024False14,450.573 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found1024False10,903.514 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found1024False9,294.073 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found1024False24,834.107 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found1024False27,807.133 ns
DictionaryTests<String>Dictionary_TryGetValue_Found1024False43,093.660 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found1024False39,181.201 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound1024False13,773.527 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound1024False8,281.616 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound1024False10,252.792 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound1024False6,199.940 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound1024False20,427.130 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound1024False15,131.280 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound1024False26,598.257 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound1024False24,474.515 ns
DictionaryTests<Guid>Dictionary_Enumerate1024False3,440.145 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate1024False1,689.686 ns
DictionaryTests<Int32>Dictionary_Enumerate1024False2,520.821 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate1024False1,498.062 ns
DictionaryTests<Object>Dictionary_Enumerate1024False4,408.578 ns
DictionaryTests<Object>FrozenDictionary_Enumerate1024False3,689.598 ns
DictionaryTests<String>Dictionary_Enumerate1024False4,398.440 ns
DictionaryTests<String>FrozenDictionary_Enumerate1024False3,636.864 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys1024False2,359.947 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys1024False261.473 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys1024False2,033.189 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys1024False253.029 ns
DictionaryTests<Object>Dictionary_EnumerateKeys1024False3,742.122 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys1024False1,226.121 ns
DictionaryTests<String>Dictionary_EnumerateKeys1024False3,693.788 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys1024False1,222.912 ns
DictionaryTests<Guid>Dictionary_EnumerateValues1024False2,144.243 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues1024False260.725 ns
DictionaryTests<Int32>Dictionary_EnumerateValues1024False2,039.401 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues1024False255.137 ns
DictionaryTests<Object>Dictionary_EnumerateValues1024False2,900.210 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues1024False254.610 ns
DictionaryTests<String>Dictionary_EnumerateValues1024False2,924.998 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues1024False252.582 ns
DictionaryTests<Guid>Dictionary_Construct1024True17,653.020 ns
DictionaryTests<Guid>FrozenDictionary_Construct1024True165,900.270 ns
DictionaryTests<Int32>Dictionary_Construct1024True15,324.775 ns
DictionaryTests<Int32>FrozenDictionary_Construct1024True145,562.184 ns
DictionaryTests<Object>Dictionary_Construct1024True22,357.810 ns
DictionaryTests<Object>FrozenDictionary_Construct1024True164,976.669 ns
DictionaryTests<String>Dictionary_Construct1024True24,584.158 ns
DictionaryTests<String>FrozenDictionary_Construct1024True713,506.562 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found1024True10,972.369 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found1024True10,677.337 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found1024True6,061.961 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found1024True4,568.878 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found1024True15,521.277 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found1024True15,039.072 ns
DictionaryTests<String>Dictionary_TryGetValue_Found1024True21,252.949 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found1024True19,273.593 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound1024True10,827.871 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound1024True6,294.780 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound1024True8,168.398 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound1024True4,152.874 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound1024True14,748.903 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound1024True9,459.935 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound1024True20,727.839 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound1024True11,941.794 ns
DictionaryTests<Guid>Dictionary_Enumerate1024True3,355.945 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate1024True1,676.260 ns
DictionaryTests<Int32>Dictionary_Enumerate1024True2,501.281 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate1024True1,488.353 ns
DictionaryTests<Object>Dictionary_Enumerate1024True4,433.532 ns
DictionaryTests<Object>FrozenDictionary_Enumerate1024True3,662.125 ns
DictionaryTests<String>Dictionary_Enumerate1024True4,485.860 ns
DictionaryTests<String>FrozenDictionary_Enumerate1024True3,652.590 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys1024True2,351.717 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys1024True262.712 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys1024True2,019.313 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys1024True252.664 ns
DictionaryTests<Object>Dictionary_EnumerateKeys1024True3,673.043 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys1024True1,224.610 ns
DictionaryTests<String>Dictionary_EnumerateKeys1024True3,748.785 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys1024True1,228.282 ns
DictionaryTests<Guid>Dictionary_EnumerateValues1024True2,080.066 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues1024True255.457 ns
DictionaryTests<Int32>Dictionary_EnumerateValues1024True2,018.673 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues1024True255.488 ns
DictionaryTests<Object>Dictionary_EnumerateValues1024True2,872.265 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues1024True255.872 ns
DictionaryTests<String>Dictionary_EnumerateValues1024True2,895.610 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues1024True254.709 ns

@ghost

ghost commented Nov 2, 2022

Copy link
Copy Markdown

Note regarding the new-api-needs-documentation label:

This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, to please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change.

@ghost

ghost commented Nov 2, 2022

Copy link
Copy Markdown

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

Issue Details

The first commit imports @geeknoid's source, updated to compile in System.Collections.Immutable.dll.

The second commit overhauls the APIs and adds tests.

This includes a bunch of updates to our shared collections tests, which we've apparently not used in earnest with read-only collections, as a bunch of mutating tests were trying to run even when IsReadOnly was set to false.

There's still more work required here. Subsequent to this PR, we should:

  • Investigate ways to make construction faster.
  • Investigate ways to make lookups faster.
  • Investigate what exact special-cased implementations we want to ship.

(@geeknoid, your help here would be appreciated. Best case numbers look very good, e.g. the example benchmarks you previously created, but others don't look nearly as good. I vetted some of the worst numbers against the original implementation, and they're similar.)

Right now, for each of dictionary/set, there are custom implementations for:

  • Int32 keys with the default comparer
  • Value type keys with the default comparer
  • String keys with default/ordinal/ordinal-ignore-case comparers. Here there are two: one that tries to bucket a small number of strings by length, and one that tries to avoid reading/hashing the full strings by selecting the smallest portion that's unique enough.
  • A general fallback implementation that's used for everything else.

We can explore other data structures, other optimizations, etc., all behind the same veneer. Some of the existing implementations might not be worthwhile, too. For some situations, we might even want an implementation that just wraps Dictionary<,> along with dedicated keys/values arrays.

But the first step is getting the APIs merged to enable others to experiment.

Some benchmarks
[InProcess][GenericTypeArguments(typeof(string))][GenericTypeArguments(typeof(int))][GenericTypeArguments(typeof(object))][GenericTypeArguments(typeof(Guid))]publicclassDictionaryTests<TKey>{privateTKey[]_keys;privateTKey[]_nonExistentKeys;privateDictionary<TKey,long>_dictionary;privateFrozenDictionary<TKey,long>_frozenDictionary;[Params(1,16,1024)]publicintCount{get;set;}[Params(false,true)]publicboolDefaultComparer{get;set;}privatestaticIEqualityComparer<TKey>GetComparer(booldefaultComparer)=>defaultComparer?EqualityComparer<TKey>.Default:NonDefaultEqualityComparer<TKey>.Instance;privateunsafestaticTRandGetRandomValue<TRand>(Randomrand){if(typeof(TRand)==typeof(string)){Span<char>span=stackallocchar[rand.Next(5,20)];for(inti=0;i<span.Length;i++){span[i]=(char)('a'+rand.Next(0,26));}return(TRand)(object)span.ToString();}if(typeof(TRand)==typeof(object)){return(TRand)newobject();}if(typeof(TRand)==typeof(Guid)){Int128value=newInt128((ulong)rand.NextInt64(),(ulong)rand.NextInt64());return(TRand)(object)(*(Guid*)(&value));}if(typeof(TRand)==typeof(int)){return(TRand)(object)rand.Next();}if(typeof(TRand)==typeof(long)){return(TRand)(object)rand.NextInt64();}thrownewException("Unknown type");}[GlobalSetup]publicvoidSetup(){varrand=newRandom(42);_dictionary=newDictionary<TKey,long>(GetComparer(DefaultComparer));for(intj=0;j<Count;j++){_dictionary[GetRandomValue<TKey>(rand)]=GetRandomValue<long>(rand);}_frozenDictionary=_dictionary.ToFrozenDictionary(GetComparer(DefaultComparer));_keys=_dictionary.Keys.ToArray();if(typeof(TKey)==typeof(string)){_keys=(TKey[])(object)Array.ConvertAll((string[])(object)_keys,string.Copy);}_nonExistentKeys=newTKey[Count];for(inti=0;i<Count;i++){TKeykey;while(_dictionary.ContainsKey(key=GetRandomValue<TKey>(rand)));_nonExistentKeys[i]=key;}}[Benchmark]publicDictionary<TKey,long>Dictionary_Construct(){return_keys.ToDictionary((TKeyk)=>k,(TKeyk)=>0L,GetComparer(DefaultComparer));}[Benchmark]publicFrozenDictionary<TKey,long>FrozenDictionary_Construct(){return_keys.ToFrozenDictionary((TKeyk)=>k,(TKeyk)=>0L,GetComparer(DefaultComparer));}[Benchmark]publicboolDictionary_TryGetValue_Found(){boolallFound=true;TKey[]keys=_keys;foreach(TKeykeyinkeys){longvalue;allFound&=_dictionary.TryGetValue(key,outvalue);}returnallFound;}[Benchmark]publicboolDictionary_TryGetValue_NotFound(){boolanyFound=false;TKey[]nonExistentKeys=_nonExistentKeys;foreach(TKeykeyinnonExistentKeys){longvalue;anyFound|=_dictionary.TryGetValue(key,outvalue);}returnanyFound;}[Benchmark]publicboolFrozenDictionary_TryGetValue_Found(){boolallFound=true;TKey[]keys=_keys;foreach(TKeykeyinkeys){longvalue;allFound&=_frozenDictionary.TryGetValue(key,outvalue);}returnallFound;}[Benchmark]publicboolFrozenDictionary_TryGetValue_NotFound(){boolanyFound=false;TKey[]nonExistentKeys=_nonExistentKeys;foreach(TKeykeyinnonExistentKeys){longvalue;anyFound|=_frozenDictionary.TryGetValue(key,outvalue);}returnanyFound;}[Benchmark]publicintDictionary_Enumerate(){intcount=0;foreach(KeyValuePair<TKey,long>itemin_dictionary){_=item;count++;}returncount;}[Benchmark]publicintDictionary_EnumerateKeys(){intcount=0;foreach(TKeykeyin_dictionary.Keys){_=key;count++;}returncount;}[Benchmark]publiclongDictionary_EnumerateValues(){intcount=0;foreach(longvaluein_dictionary.Values){_=value;count++;}returncount;}[Benchmark]publicintFrozenDictionary_Enumerate(){intcount=0;foreach(KeyValuePair<TKey,long>itemin_frozenDictionary){_=item;count++;}returncount;}[Benchmark]publicintFrozenDictionary_EnumerateKeys(){intcount=0;foreach(TKeykeyin_frozenDictionary.Keys){count++;}returncount;}[Benchmark]publicintFrozenDictionary_EnumerateValues(){intcount=0;foreach(longvaluein_frozenDictionary.Values){count++;}returncount;}}internalsealedpartialclassNonDefaultEqualityComparer<T>:EqualityComparer<T>{publicstaticNonDefaultEqualityComparer<T>Instance{get;}=newNonDefaultEqualityComparer<T>();publicoverrideboolEquals(Tx,Ty)=>Default.Equals(x,y);publicoverrideintGetHashCode(Tobj)=>Default.GetHashCode(obj);}
TypeMethodCountDefaultComparerMean
DictionaryTests<Guid>Dictionary_Construct16False308.917 ns
DictionaryTests<Guid>FrozenDictionary_Construct16False2,546.966 ns
DictionaryTests<Int32>Dictionary_Construct16False256.919 ns
DictionaryTests<Int32>FrozenDictionary_Construct16False2,735.513 ns
DictionaryTests<Object>Dictionary_Construct16False459.599 ns
DictionaryTests<Object>FrozenDictionary_Construct16False2,990.372 ns
DictionaryTests<String>Dictionary_Construct16False574.025 ns
DictionaryTests<String>FrozenDictionary_Construct16False3,085.790 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found16False195.613 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found16False180.628 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found16False127.828 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found16False120.595 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found16False371.343 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found16False384.100 ns
DictionaryTests<String>Dictionary_TryGetValue_Found16False559.943 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found16False531.615 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound16False149.167 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound16False120.109 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound16False109.596 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound16False91.319 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound16False247.097 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound16False231.592 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound16False356.568 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound16False334.678 ns
DictionaryTests<Guid>Dictionary_Enumerate16False61.120 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate16False27.948 ns
DictionaryTests<Int32>Dictionary_Enumerate16False50.372 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate16False27.162 ns
DictionaryTests<Object>Dictionary_Enumerate16False89.827 ns
DictionaryTests<Object>FrozenDictionary_Enumerate16False74.392 ns
DictionaryTests<String>Dictionary_Enumerate16False89.446 ns
DictionaryTests<String>FrozenDictionary_Enumerate16False74.626 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys16False49.057 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys16False4.847 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys16False37.000 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys16False5.108 ns
DictionaryTests<Object>Dictionary_EnumerateKeys16False74.497 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys16False18.488 ns
DictionaryTests<String>Dictionary_EnumerateKeys16False74.906 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys16False18.571 ns
DictionaryTests<Guid>Dictionary_EnumerateValues16False38.145 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues16False5.054 ns
DictionaryTests<Int32>Dictionary_EnumerateValues16False36.834 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues16False5.103 ns
DictionaryTests<Object>Dictionary_EnumerateValues16False54.844 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues16False5.045 ns
DictionaryTests<String>Dictionary_EnumerateValues16False55.299 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues16False5.048 ns
DictionaryTests<Guid>Dictionary_Construct16True276.233 ns
DictionaryTests<Guid>FrozenDictionary_Construct16True2,480.023 ns
DictionaryTests<Int32>Dictionary_Construct16True247.043 ns
DictionaryTests<Int32>FrozenDictionary_Construct16True2,679.061 ns
DictionaryTests<Object>Dictionary_Construct16True401.734 ns
DictionaryTests<Object>FrozenDictionary_Construct16True2,782.085 ns
DictionaryTests<String>Dictionary_Construct16True360.930 ns
DictionaryTests<String>FrozenDictionary_Construct16True1,385.360 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found16True116.494 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found16True124.232 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found16True87.759 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found16True50.979 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found16True207.784 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found16True209.461 ns
DictionaryTests<String>Dictionary_TryGetValue_Found16True268.939 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found16True142.170 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound16True99.116 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound16True81.180 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound16True72.956 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound16True53.239 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound16True163.937 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound16True144.715 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound16True178.219 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound16True109.456 ns
DictionaryTests<Guid>Dictionary_Enumerate16True58.001 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate16True30.898 ns
DictionaryTests<Int32>Dictionary_Enumerate16True49.878 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate16True27.879 ns
DictionaryTests<Object>Dictionary_Enumerate16True89.847 ns
DictionaryTests<Object>FrozenDictionary_Enumerate16True74.568 ns
DictionaryTests<String>Dictionary_Enumerate16True89.347 ns
DictionaryTests<String>FrozenDictionary_Enumerate16True73.377 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys16True48.112 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys16True5.259 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys16True36.038 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys16True5.016 ns
DictionaryTests<Object>Dictionary_EnumerateKeys16True74.331 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys16True19.002 ns
DictionaryTests<String>Dictionary_EnumerateKeys16True73.520 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys16True18.961 ns
DictionaryTests<Guid>Dictionary_EnumerateValues16True39.389 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues16True5.432 ns
DictionaryTests<Int32>Dictionary_EnumerateValues16True37.072 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues16True4.999 ns
DictionaryTests<Object>Dictionary_EnumerateValues16True54.973 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues16True5.033 ns
DictionaryTests<String>Dictionary_EnumerateValues16True54.496 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues16True5.029 ns
DictionaryTests<Guid>Dictionary_Construct1024False20,251.116 ns
DictionaryTests<Guid>FrozenDictionary_Construct1024False165,973.254 ns
DictionaryTests<Int32>Dictionary_Construct1024False16,858.288 ns
DictionaryTests<Int32>FrozenDictionary_Construct1024False149,391.455 ns
DictionaryTests<Object>Dictionary_Construct1024False26,154.444 ns
DictionaryTests<Object>FrozenDictionary_Construct1024False174,545.846 ns
DictionaryTests<String>Dictionary_Construct1024False36,381.752 ns
DictionaryTests<String>FrozenDictionary_Construct1024False200,478.010 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found1024False16,232.339 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found1024False14,450.573 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found1024False10,903.514 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found1024False9,294.073 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found1024False24,834.107 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found1024False27,807.133 ns
DictionaryTests<String>Dictionary_TryGetValue_Found1024False43,093.660 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found1024False39,181.201 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound1024False13,773.527 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound1024False8,281.616 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound1024False10,252.792 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound1024False6,199.940 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound1024False20,427.130 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound1024False15,131.280 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound1024False26,598.257 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound1024False24,474.515 ns
DictionaryTests<Guid>Dictionary_Enumerate1024False3,440.145 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate1024False1,689.686 ns
DictionaryTests<Int32>Dictionary_Enumerate1024False2,520.821 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate1024False1,498.062 ns
DictionaryTests<Object>Dictionary_Enumerate1024False4,408.578 ns
DictionaryTests<Object>FrozenDictionary_Enumerate1024False3,689.598 ns
DictionaryTests<String>Dictionary_Enumerate1024False4,398.440 ns
DictionaryTests<String>FrozenDictionary_Enumerate1024False3,636.864 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys1024False2,359.947 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys1024False261.473 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys1024False2,033.189 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys1024False253.029 ns
DictionaryTests<Object>Dictionary_EnumerateKeys1024False3,742.122 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys1024False1,226.121 ns
DictionaryTests<String>Dictionary_EnumerateKeys1024False3,693.788 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys1024False1,222.912 ns
DictionaryTests<Guid>Dictionary_EnumerateValues1024False2,144.243 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues1024False260.725 ns
DictionaryTests<Int32>Dictionary_EnumerateValues1024False2,039.401 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues1024False255.137 ns
DictionaryTests<Object>Dictionary_EnumerateValues1024False2,900.210 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues1024False254.610 ns
DictionaryTests<String>Dictionary_EnumerateValues1024False2,924.998 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues1024False252.582 ns
DictionaryTests<Guid>Dictionary_Construct1024True17,653.020 ns
DictionaryTests<Guid>FrozenDictionary_Construct1024True165,900.270 ns
DictionaryTests<Int32>Dictionary_Construct1024True15,324.775 ns
DictionaryTests<Int32>FrozenDictionary_Construct1024True145,562.184 ns
DictionaryTests<Object>Dictionary_Construct1024True22,357.810 ns
DictionaryTests<Object>FrozenDictionary_Construct1024True164,976.669 ns
DictionaryTests<String>Dictionary_Construct1024True24,584.158 ns
DictionaryTests<String>FrozenDictionary_Construct1024True713,506.562 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found1024True10,972.369 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found1024True10,677.337 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found1024True6,061.961 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found1024True4,568.878 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found1024True15,521.277 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found1024True15,039.072 ns
DictionaryTests<String>Dictionary_TryGetValue_Found1024True21,252.949 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found1024True19,273.593 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound1024True10,827.871 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound1024True6,294.780 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound1024True8,168.398 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound1024True4,152.874 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound1024True14,748.903 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound1024True9,459.935 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound1024True20,727.839 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound1024True11,941.794 ns
DictionaryTests<Guid>Dictionary_Enumerate1024True3,355.945 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate1024True1,676.260 ns
DictionaryTests<Int32>Dictionary_Enumerate1024True2,501.281 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate1024True1,488.353 ns
DictionaryTests<Object>Dictionary_Enumerate1024True4,433.532 ns
DictionaryTests<Object>FrozenDictionary_Enumerate1024True3,662.125 ns
DictionaryTests<String>Dictionary_Enumerate1024True4,485.860 ns
DictionaryTests<String>FrozenDictionary_Enumerate1024True3,652.590 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys1024True2,351.717 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys1024True262.712 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys1024True2,019.313 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys1024True252.664 ns
DictionaryTests<Object>Dictionary_EnumerateKeys1024True3,673.043 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys1024True1,224.610 ns
DictionaryTests<String>Dictionary_EnumerateKeys1024True3,748.785 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys1024True1,228.282 ns
DictionaryTests<Guid>Dictionary_EnumerateValues1024True2,080.066 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues1024True255.457 ns
DictionaryTests<Int32>Dictionary_EnumerateValues1024True2,018.673 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues1024True255.488 ns
DictionaryTests<Object>Dictionary_EnumerateValues1024True2,872.265 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues1024True255.872 ns
DictionaryTests<String>Dictionary_EnumerateValues1024True2,895.610 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues1024True254.709 ns
Author:stephentoub
Assignees:-
Labels:

area-System.Collections

Milestone:8.0.0

@ghostghost assigned stephentoubNov 2, 2022
@geeknoid

Copy link
Copy Markdown
Member

The read benchmarks look pretty good, but the construction phase is definitely on the slow side. I never really measure the construction time since for my scenarios I didn't care so much. I do think adding some notion of "intensity" in the constructor to let the caller decide how much time to spend optimizing would be desirable.

It would be interesting in particular to see what the read perf is if the implementation doesn't perform any optimization at all. It would likely still be faster than a normal Dictionary/HashSet when using the default comparers, but not by much. But if that case is still a bit faster and construction time is on par with normal Dictionary/HashSet, then that says that any scenario that needs a readonly dictionary or set should be using the frozen ones by default since it would be faster.

Without the ability to tune how much time is spent constructing, then you have to be careful where you use the frozen collections and make sure the cost/benefit will be worth it.

@geeknoid

geeknoid commented Nov 2, 2022

Copy link
Copy Markdown
Member

Some thoughts for the future:

  • Dictionaries and sets with low cardinality are common. We should look at adding specialized implementation for 1 and 2 entry collections. This saves memory and speeds things up.

  • I had on my todo list to create specialized two character string comparers to optimize that case. For small collections, often the single char comparer is enough and works well. But when the collection grows, you run out of single characters to compare, so the code falls back to two.

  • We discussed in the review that the majority use case for this kind of collection is where the data comes dynamically into the process. This says that doing the optimization work for the collections in a source generator is not likely to be used frequently. However, what I think might be useful is the ability to serialize/deserialize the internal state for the frozen collections in a portable way. This would let me distribute this serialized state as config, and thus forego the construction overhead. So instead of putting a bunch of key/value pairs in my config and having to optimize those in each of my service's processes, I could optimize the key/value pairs offline and push the resulting state to the processes.

stephentouband others added 2 commits November 2, 2022 17:45
…y compile
Co-authored-by: Martin Taillefer <mataille@microsoft.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>

@geeknoidgeeknoid left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This looks great.

@krwq

krwq commented Nov 4, 2022

Copy link
Copy Markdown
Member

I'd be really interested to see how integration of this feature would look like with System.Text.Json (both JsonSerializerOptions.Converters and JsonTypeInfo.Properties are freezing values at one point)

krwq
krwq approved these changes Nov 4, 2022

@krwqkrwq left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I've skimmed all of the files and overall looks good. I'm really impressed in the amount of heuristics done here to make this super optimized. Thank you!

@stephentoub
stephentoub merged commit 6c5a440 into dotnet:mainNov 4, 2022
@stephentoub
stephentoub deleted the frozencollections branch November 4, 2022 19:49
@ghostghost locked as resolved and limited conversation to collaborators Dec 5, 2022
@jeffhandleyjeffhandley added the blog-candidate Completed PRs that are candidate topics for blog post coverage label Mar 14, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Collectionsblog-candidateCompleted PRs that are candidate topics for blog post coveragenew-api-needs-documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[API Proposal]: Provide optimized read-only collections

4 participants

@stephentoub@geeknoid@krwq@jeffhandley
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add FrozenDictionary/Set - #77799

Merged
stephentoub merged 3 commits into
dotnet:mainfrom
stephentoub:frozencollections
Nov 4, 2022
Merged

Add FrozenDictionary/Set#77799
stephentoub merged 3 commits into
dotnet:mainfrom
stephentoub:frozencollections

Conversation

@stephentoub

@stephentoubstephentoub commented Nov 2, 2022

Copy link
Copy Markdown
Member

Fixes#67209

The first commit imports @geeknoid's source, updated to compile in System.Collections.Immutable.dll.

The second commit overhauls the APIs and adds tests. Overhauling the APIs also required lots of surgery on various aspects of the implementation.

This includes a bunch of updates to our shared collections tests, which we've apparently not used in earnest with read-only collections, as a bunch of mutating tests were trying to run even when IsReadOnly was set to false.

There's still more work required here. Subsequent to this PR, we should:

  • Investigate ways to make construction faster.
  • Investigate ways to make lookups faster.
  • Investigate what exact special-cased implementations we want to ship.

(@geeknoid, your help here would be appreciated. Best case numbers look very good, e.g. the example benchmarks you previously created, but others don't look nearly as good. I vetted some of the worst numbers against the original implementation, and they're similar.)

Right now, for each of dictionary/set, there are custom implementations for:

  • Int32 keys with the default comparer
  • Value type keys with the default comparer
  • String keys with default/ordinal/ordinal-ignore-case comparers. Here there are two: one that tries to bucket a small number of strings by length, and one that tries to avoid reading/hashing the full strings by selecting the smallest portion that's unique enough.
  • A general fallback implementation that's used for everything else.

We can explore other data structures, other optimizations, etc., all behind the same veneer. Some of the existing implementations might not be worthwhile, too. For some situations, we might even want an implementation that just wraps Dictionary<,> along with dedicated keys/values arrays.

But the first step is getting the APIs merged to enable others to experiment.

Some benchmarks
[InProcess][GenericTypeArguments(typeof(string))][GenericTypeArguments(typeof(int))][GenericTypeArguments(typeof(object))][GenericTypeArguments(typeof(Guid))]publicclassDictionaryTests<TKey>{privateTKey[]_keys;privateTKey[]_nonExistentKeys;privateDictionary<TKey,long>_dictionary;privateFrozenDictionary<TKey,long>_frozenDictionary;[Params(1,16,1024)]publicintCount{get;set;}[Params(false,true)]publicboolDefaultComparer{get;set;}privatestaticIEqualityComparer<TKey>GetComparer(booldefaultComparer)=>defaultComparer?EqualityComparer<TKey>.Default:NonDefaultEqualityComparer<TKey>.Instance;privateunsafestaticTRandGetRandomValue<TRand>(Randomrand){if(typeof(TRand)==typeof(string)){Span<char>span=stackallocchar[rand.Next(5,20)];for(inti=0;i<span.Length;i++){span[i]=(char)('a'+rand.Next(0,26));}return(TRand)(object)span.ToString();}if(typeof(TRand)==typeof(object)){return(TRand)newobject();}if(typeof(TRand)==typeof(Guid)){Int128value=newInt128((ulong)rand.NextInt64(),(ulong)rand.NextInt64());return(TRand)(object)(*(Guid*)(&value));}if(typeof(TRand)==typeof(int)){return(TRand)(object)rand.Next();}if(typeof(TRand)==typeof(long)){return(TRand)(object)rand.NextInt64();}thrownewException("Unknown type");}[GlobalSetup]publicvoidSetup(){varrand=newRandom(42);_dictionary=newDictionary<TKey,long>(GetComparer(DefaultComparer));for(intj=0;j<Count;j++){_dictionary[GetRandomValue<TKey>(rand)]=GetRandomValue<long>(rand);}_frozenDictionary=_dictionary.ToFrozenDictionary(GetComparer(DefaultComparer));_keys=_dictionary.Keys.ToArray();if(typeof(TKey)==typeof(string)){_keys=(TKey[])(object)Array.ConvertAll((string[])(object)_keys,string.Copy);}_nonExistentKeys=newTKey[Count];for(inti=0;i<Count;i++){TKeykey;while(_dictionary.ContainsKey(key=GetRandomValue<TKey>(rand)));_nonExistentKeys[i]=key;}}[Benchmark]publicDictionary<TKey,long>Dictionary_Construct(){return_keys.ToDictionary((TKeyk)=>k,(TKeyk)=>0L,GetComparer(DefaultComparer));}[Benchmark]publicFrozenDictionary<TKey,long>FrozenDictionary_Construct(){return_keys.ToFrozenDictionary((TKeyk)=>k,(TKeyk)=>0L,GetComparer(DefaultComparer));}[Benchmark]publicboolDictionary_TryGetValue_Found(){boolallFound=true;TKey[]keys=_keys;foreach(TKeykeyinkeys){longvalue;allFound&=_dictionary.TryGetValue(key,outvalue);}returnallFound;}[Benchmark]publicboolDictionary_TryGetValue_NotFound(){boolanyFound=false;TKey[]nonExistentKeys=_nonExistentKeys;foreach(TKeykeyinnonExistentKeys){longvalue;anyFound|=_dictionary.TryGetValue(key,outvalue);}returnanyFound;}[Benchmark]publicboolFrozenDictionary_TryGetValue_Found(){boolallFound=true;TKey[]keys=_keys;foreach(TKeykeyinkeys){longvalue;allFound&=_frozenDictionary.TryGetValue(key,outvalue);}returnallFound;}[Benchmark]publicboolFrozenDictionary_TryGetValue_NotFound(){boolanyFound=false;TKey[]nonExistentKeys=_nonExistentKeys;foreach(TKeykeyinnonExistentKeys){longvalue;anyFound|=_frozenDictionary.TryGetValue(key,outvalue);}returnanyFound;}[Benchmark]publicintDictionary_Enumerate(){intcount=0;foreach(KeyValuePair<TKey,long>itemin_dictionary){_=item;count++;}returncount;}[Benchmark]publicintDictionary_EnumerateKeys(){intcount=0;foreach(TKeykeyin_dictionary.Keys){_=key;count++;}returncount;}[Benchmark]publiclongDictionary_EnumerateValues(){intcount=0;foreach(longvaluein_dictionary.Values){_=value;count++;}returncount;}[Benchmark]publicintFrozenDictionary_Enumerate(){intcount=0;foreach(KeyValuePair<TKey,long>itemin_frozenDictionary){_=item;count++;}returncount;}[Benchmark]publicintFrozenDictionary_EnumerateKeys(){intcount=0;foreach(TKeykeyin_frozenDictionary.Keys){count++;}returncount;}[Benchmark]publicintFrozenDictionary_EnumerateValues(){intcount=0;foreach(longvaluein_frozenDictionary.Values){count++;}returncount;}}internalsealedpartialclassNonDefaultEqualityComparer<T>:EqualityComparer<T>{publicstaticNonDefaultEqualityComparer<T>Instance{get;}=newNonDefaultEqualityComparer<T>();publicoverrideboolEquals(Tx,Ty)=>Default.Equals(x,y);publicoverrideintGetHashCode(Tobj)=>Default.GetHashCode(obj);}
TypeMethodCountDefaultComparerMean
DictionaryTests<Guid>Dictionary_Construct16False308.917 ns
DictionaryTests<Guid>FrozenDictionary_Construct16False2,546.966 ns
DictionaryTests<Int32>Dictionary_Construct16False256.919 ns
DictionaryTests<Int32>FrozenDictionary_Construct16False2,735.513 ns
DictionaryTests<Object>Dictionary_Construct16False459.599 ns
DictionaryTests<Object>FrozenDictionary_Construct16False2,990.372 ns
DictionaryTests<String>Dictionary_Construct16False574.025 ns
DictionaryTests<String>FrozenDictionary_Construct16False3,085.790 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found16False195.613 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found16False180.628 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found16False127.828 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found16False120.595 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found16False371.343 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found16False384.100 ns
DictionaryTests<String>Dictionary_TryGetValue_Found16False559.943 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found16False531.615 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound16False149.167 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound16False120.109 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound16False109.596 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound16False91.319 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound16False247.097 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound16False231.592 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound16False356.568 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound16False334.678 ns
DictionaryTests<Guid>Dictionary_Enumerate16False61.120 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate16False27.948 ns
DictionaryTests<Int32>Dictionary_Enumerate16False50.372 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate16False27.162 ns
DictionaryTests<Object>Dictionary_Enumerate16False89.827 ns
DictionaryTests<Object>FrozenDictionary_Enumerate16False74.392 ns
DictionaryTests<String>Dictionary_Enumerate16False89.446 ns
DictionaryTests<String>FrozenDictionary_Enumerate16False74.626 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys16False49.057 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys16False4.847 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys16False37.000 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys16False5.108 ns
DictionaryTests<Object>Dictionary_EnumerateKeys16False74.497 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys16False18.488 ns
DictionaryTests<String>Dictionary_EnumerateKeys16False74.906 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys16False18.571 ns
DictionaryTests<Guid>Dictionary_EnumerateValues16False38.145 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues16False5.054 ns
DictionaryTests<Int32>Dictionary_EnumerateValues16False36.834 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues16False5.103 ns
DictionaryTests<Object>Dictionary_EnumerateValues16False54.844 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues16False5.045 ns
DictionaryTests<String>Dictionary_EnumerateValues16False55.299 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues16False5.048 ns
DictionaryTests<Guid>Dictionary_Construct16True276.233 ns
DictionaryTests<Guid>FrozenDictionary_Construct16True2,480.023 ns
DictionaryTests<Int32>Dictionary_Construct16True247.043 ns
DictionaryTests<Int32>FrozenDictionary_Construct16True2,679.061 ns
DictionaryTests<Object>Dictionary_Construct16True401.734 ns
DictionaryTests<Object>FrozenDictionary_Construct16True2,782.085 ns
DictionaryTests<String>Dictionary_Construct16True360.930 ns
DictionaryTests<String>FrozenDictionary_Construct16True1,385.360 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found16True116.494 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found16True124.232 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found16True87.759 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found16True50.979 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found16True207.784 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found16True209.461 ns
DictionaryTests<String>Dictionary_TryGetValue_Found16True268.939 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found16True142.170 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound16True99.116 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound16True81.180 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound16True72.956 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound16True53.239 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound16True163.937 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound16True144.715 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound16True178.219 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound16True109.456 ns
DictionaryTests<Guid>Dictionary_Enumerate16True58.001 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate16True30.898 ns
DictionaryTests<Int32>Dictionary_Enumerate16True49.878 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate16True27.879 ns
DictionaryTests<Object>Dictionary_Enumerate16True89.847 ns
DictionaryTests<Object>FrozenDictionary_Enumerate16True74.568 ns
DictionaryTests<String>Dictionary_Enumerate16True89.347 ns
DictionaryTests<String>FrozenDictionary_Enumerate16True73.377 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys16True48.112 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys16True5.259 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys16True36.038 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys16True5.016 ns
DictionaryTests<Object>Dictionary_EnumerateKeys16True74.331 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys16True19.002 ns
DictionaryTests<String>Dictionary_EnumerateKeys16True73.520 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys16True18.961 ns
DictionaryTests<Guid>Dictionary_EnumerateValues16True39.389 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues16True5.432 ns
DictionaryTests<Int32>Dictionary_EnumerateValues16True37.072 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues16True4.999 ns
DictionaryTests<Object>Dictionary_EnumerateValues16True54.973 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues16True5.033 ns
DictionaryTests<String>Dictionary_EnumerateValues16True54.496 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues16True5.029 ns
DictionaryTests<Guid>Dictionary_Construct1024False20,251.116 ns
DictionaryTests<Guid>FrozenDictionary_Construct1024False165,973.254 ns
DictionaryTests<Int32>Dictionary_Construct1024False16,858.288 ns
DictionaryTests<Int32>FrozenDictionary_Construct1024False149,391.455 ns
DictionaryTests<Object>Dictionary_Construct1024False26,154.444 ns
DictionaryTests<Object>FrozenDictionary_Construct1024False174,545.846 ns
DictionaryTests<String>Dictionary_Construct1024False36,381.752 ns
DictionaryTests<String>FrozenDictionary_Construct1024False200,478.010 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found1024False16,232.339 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found1024False14,450.573 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found1024False10,903.514 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found1024False9,294.073 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found1024False24,834.107 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found1024False27,807.133 ns
DictionaryTests<String>Dictionary_TryGetValue_Found1024False43,093.660 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found1024False39,181.201 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound1024False13,773.527 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound1024False8,281.616 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound1024False10,252.792 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound1024False6,199.940 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound1024False20,427.130 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound1024False15,131.280 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound1024False26,598.257 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound1024False24,474.515 ns
DictionaryTests<Guid>Dictionary_Enumerate1024False3,440.145 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate1024False1,689.686 ns
DictionaryTests<Int32>Dictionary_Enumerate1024False2,520.821 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate1024False1,498.062 ns
DictionaryTests<Object>Dictionary_Enumerate1024False4,408.578 ns
DictionaryTests<Object>FrozenDictionary_Enumerate1024False3,689.598 ns
DictionaryTests<String>Dictionary_Enumerate1024False4,398.440 ns
DictionaryTests<String>FrozenDictionary_Enumerate1024False3,636.864 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys1024False2,359.947 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys1024False261.473 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys1024False2,033.189 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys1024False253.029 ns
DictionaryTests<Object>Dictionary_EnumerateKeys1024False3,742.122 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys1024False1,226.121 ns
DictionaryTests<String>Dictionary_EnumerateKeys1024False3,693.788 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys1024False1,222.912 ns
DictionaryTests<Guid>Dictionary_EnumerateValues1024False2,144.243 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues1024False260.725 ns
DictionaryTests<Int32>Dictionary_EnumerateValues1024False2,039.401 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues1024False255.137 ns
DictionaryTests<Object>Dictionary_EnumerateValues1024False2,900.210 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues1024False254.610 ns
DictionaryTests<String>Dictionary_EnumerateValues1024False2,924.998 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues1024False252.582 ns
DictionaryTests<Guid>Dictionary_Construct1024True17,653.020 ns
DictionaryTests<Guid>FrozenDictionary_Construct1024True165,900.270 ns
DictionaryTests<Int32>Dictionary_Construct1024True15,324.775 ns
DictionaryTests<Int32>FrozenDictionary_Construct1024True145,562.184 ns
DictionaryTests<Object>Dictionary_Construct1024True22,357.810 ns
DictionaryTests<Object>FrozenDictionary_Construct1024True164,976.669 ns
DictionaryTests<String>Dictionary_Construct1024True24,584.158 ns
DictionaryTests<String>FrozenDictionary_Construct1024True713,506.562 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found1024True10,972.369 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found1024True10,677.337 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found1024True6,061.961 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found1024True4,568.878 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found1024True15,521.277 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found1024True15,039.072 ns
DictionaryTests<String>Dictionary_TryGetValue_Found1024True21,252.949 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found1024True19,273.593 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound1024True10,827.871 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound1024True6,294.780 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound1024True8,168.398 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound1024True4,152.874 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound1024True14,748.903 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound1024True9,459.935 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound1024True20,727.839 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound1024True11,941.794 ns
DictionaryTests<Guid>Dictionary_Enumerate1024True3,355.945 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate1024True1,676.260 ns
DictionaryTests<Int32>Dictionary_Enumerate1024True2,501.281 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate1024True1,488.353 ns
DictionaryTests<Object>Dictionary_Enumerate1024True4,433.532 ns
DictionaryTests<Object>FrozenDictionary_Enumerate1024True3,662.125 ns
DictionaryTests<String>Dictionary_Enumerate1024True4,485.860 ns
DictionaryTests<String>FrozenDictionary_Enumerate1024True3,652.590 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys1024True2,351.717 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys1024True262.712 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys1024True2,019.313 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys1024True252.664 ns
DictionaryTests<Object>Dictionary_EnumerateKeys1024True3,673.043 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys1024True1,224.610 ns
DictionaryTests<String>Dictionary_EnumerateKeys1024True3,748.785 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys1024True1,228.282 ns
DictionaryTests<Guid>Dictionary_EnumerateValues1024True2,080.066 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues1024True255.457 ns
DictionaryTests<Int32>Dictionary_EnumerateValues1024True2,018.673 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues1024True255.488 ns
DictionaryTests<Object>Dictionary_EnumerateValues1024True2,872.265 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues1024True255.872 ns
DictionaryTests<String>Dictionary_EnumerateValues1024True2,895.610 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues1024True254.709 ns

@ghost

ghost commented Nov 2, 2022

Copy link
Copy Markdown

Note regarding the new-api-needs-documentation label:

This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, to please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change.

@ghost

ghost commented Nov 2, 2022

Copy link
Copy Markdown

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

Issue Details

The first commit imports @geeknoid's source, updated to compile in System.Collections.Immutable.dll.

The second commit overhauls the APIs and adds tests.

This includes a bunch of updates to our shared collections tests, which we've apparently not used in earnest with read-only collections, as a bunch of mutating tests were trying to run even when IsReadOnly was set to false.

There's still more work required here. Subsequent to this PR, we should:

  • Investigate ways to make construction faster.
  • Investigate ways to make lookups faster.
  • Investigate what exact special-cased implementations we want to ship.

(@geeknoid, your help here would be appreciated. Best case numbers look very good, e.g. the example benchmarks you previously created, but others don't look nearly as good. I vetted some of the worst numbers against the original implementation, and they're similar.)

Right now, for each of dictionary/set, there are custom implementations for:

  • Int32 keys with the default comparer
  • Value type keys with the default comparer
  • String keys with default/ordinal/ordinal-ignore-case comparers. Here there are two: one that tries to bucket a small number of strings by length, and one that tries to avoid reading/hashing the full strings by selecting the smallest portion that's unique enough.
  • A general fallback implementation that's used for everything else.

We can explore other data structures, other optimizations, etc., all behind the same veneer. Some of the existing implementations might not be worthwhile, too. For some situations, we might even want an implementation that just wraps Dictionary<,> along with dedicated keys/values arrays.

But the first step is getting the APIs merged to enable others to experiment.

Some benchmarks
[InProcess][GenericTypeArguments(typeof(string))][GenericTypeArguments(typeof(int))][GenericTypeArguments(typeof(object))][GenericTypeArguments(typeof(Guid))]publicclassDictionaryTests<TKey>{privateTKey[]_keys;privateTKey[]_nonExistentKeys;privateDictionary<TKey,long>_dictionary;privateFrozenDictionary<TKey,long>_frozenDictionary;[Params(1,16,1024)]publicintCount{get;set;}[Params(false,true)]publicboolDefaultComparer{get;set;}privatestaticIEqualityComparer<TKey>GetComparer(booldefaultComparer)=>defaultComparer?EqualityComparer<TKey>.Default:NonDefaultEqualityComparer<TKey>.Instance;privateunsafestaticTRandGetRandomValue<TRand>(Randomrand){if(typeof(TRand)==typeof(string)){Span<char>span=stackallocchar[rand.Next(5,20)];for(inti=0;i<span.Length;i++){span[i]=(char)('a'+rand.Next(0,26));}return(TRand)(object)span.ToString();}if(typeof(TRand)==typeof(object)){return(TRand)newobject();}if(typeof(TRand)==typeof(Guid)){Int128value=newInt128((ulong)rand.NextInt64(),(ulong)rand.NextInt64());return(TRand)(object)(*(Guid*)(&value));}if(typeof(TRand)==typeof(int)){return(TRand)(object)rand.Next();}if(typeof(TRand)==typeof(long)){return(TRand)(object)rand.NextInt64();}thrownewException("Unknown type");}[GlobalSetup]publicvoidSetup(){varrand=newRandom(42);_dictionary=newDictionary<TKey,long>(GetComparer(DefaultComparer));for(intj=0;j<Count;j++){_dictionary[GetRandomValue<TKey>(rand)]=GetRandomValue<long>(rand);}_frozenDictionary=_dictionary.ToFrozenDictionary(GetComparer(DefaultComparer));_keys=_dictionary.Keys.ToArray();if(typeof(TKey)==typeof(string)){_keys=(TKey[])(object)Array.ConvertAll((string[])(object)_keys,string.Copy);}_nonExistentKeys=newTKey[Count];for(inti=0;i<Count;i++){TKeykey;while(_dictionary.ContainsKey(key=GetRandomValue<TKey>(rand)));_nonExistentKeys[i]=key;}}[Benchmark]publicDictionary<TKey,long>Dictionary_Construct(){return_keys.ToDictionary((TKeyk)=>k,(TKeyk)=>0L,GetComparer(DefaultComparer));}[Benchmark]publicFrozenDictionary<TKey,long>FrozenDictionary_Construct(){return_keys.ToFrozenDictionary((TKeyk)=>k,(TKeyk)=>0L,GetComparer(DefaultComparer));}[Benchmark]publicboolDictionary_TryGetValue_Found(){boolallFound=true;TKey[]keys=_keys;foreach(TKeykeyinkeys){longvalue;allFound&=_dictionary.TryGetValue(key,outvalue);}returnallFound;}[Benchmark]publicboolDictionary_TryGetValue_NotFound(){boolanyFound=false;TKey[]nonExistentKeys=_nonExistentKeys;foreach(TKeykeyinnonExistentKeys){longvalue;anyFound|=_dictionary.TryGetValue(key,outvalue);}returnanyFound;}[Benchmark]publicboolFrozenDictionary_TryGetValue_Found(){boolallFound=true;TKey[]keys=_keys;foreach(TKeykeyinkeys){longvalue;allFound&=_frozenDictionary.TryGetValue(key,outvalue);}returnallFound;}[Benchmark]publicboolFrozenDictionary_TryGetValue_NotFound(){boolanyFound=false;TKey[]nonExistentKeys=_nonExistentKeys;foreach(TKeykeyinnonExistentKeys){longvalue;anyFound|=_frozenDictionary.TryGetValue(key,outvalue);}returnanyFound;}[Benchmark]publicintDictionary_Enumerate(){intcount=0;foreach(KeyValuePair<TKey,long>itemin_dictionary){_=item;count++;}returncount;}[Benchmark]publicintDictionary_EnumerateKeys(){intcount=0;foreach(TKeykeyin_dictionary.Keys){_=key;count++;}returncount;}[Benchmark]publiclongDictionary_EnumerateValues(){intcount=0;foreach(longvaluein_dictionary.Values){_=value;count++;}returncount;}[Benchmark]publicintFrozenDictionary_Enumerate(){intcount=0;foreach(KeyValuePair<TKey,long>itemin_frozenDictionary){_=item;count++;}returncount;}[Benchmark]publicintFrozenDictionary_EnumerateKeys(){intcount=0;foreach(TKeykeyin_frozenDictionary.Keys){count++;}returncount;}[Benchmark]publicintFrozenDictionary_EnumerateValues(){intcount=0;foreach(longvaluein_frozenDictionary.Values){count++;}returncount;}}internalsealedpartialclassNonDefaultEqualityComparer<T>:EqualityComparer<T>{publicstaticNonDefaultEqualityComparer<T>Instance{get;}=newNonDefaultEqualityComparer<T>();publicoverrideboolEquals(Tx,Ty)=>Default.Equals(x,y);publicoverrideintGetHashCode(Tobj)=>Default.GetHashCode(obj);}
TypeMethodCountDefaultComparerMean
DictionaryTests<Guid>Dictionary_Construct16False308.917 ns
DictionaryTests<Guid>FrozenDictionary_Construct16False2,546.966 ns
DictionaryTests<Int32>Dictionary_Construct16False256.919 ns
DictionaryTests<Int32>FrozenDictionary_Construct16False2,735.513 ns
DictionaryTests<Object>Dictionary_Construct16False459.599 ns
DictionaryTests<Object>FrozenDictionary_Construct16False2,990.372 ns
DictionaryTests<String>Dictionary_Construct16False574.025 ns
DictionaryTests<String>FrozenDictionary_Construct16False3,085.790 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found16False195.613 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found16False180.628 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found16False127.828 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found16False120.595 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found16False371.343 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found16False384.100 ns
DictionaryTests<String>Dictionary_TryGetValue_Found16False559.943 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found16False531.615 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound16False149.167 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound16False120.109 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound16False109.596 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound16False91.319 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound16False247.097 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound16False231.592 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound16False356.568 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound16False334.678 ns
DictionaryTests<Guid>Dictionary_Enumerate16False61.120 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate16False27.948 ns
DictionaryTests<Int32>Dictionary_Enumerate16False50.372 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate16False27.162 ns
DictionaryTests<Object>Dictionary_Enumerate16False89.827 ns
DictionaryTests<Object>FrozenDictionary_Enumerate16False74.392 ns
DictionaryTests<String>Dictionary_Enumerate16False89.446 ns
DictionaryTests<String>FrozenDictionary_Enumerate16False74.626 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys16False49.057 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys16False4.847 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys16False37.000 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys16False5.108 ns
DictionaryTests<Object>Dictionary_EnumerateKeys16False74.497 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys16False18.488 ns
DictionaryTests<String>Dictionary_EnumerateKeys16False74.906 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys16False18.571 ns
DictionaryTests<Guid>Dictionary_EnumerateValues16False38.145 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues16False5.054 ns
DictionaryTests<Int32>Dictionary_EnumerateValues16False36.834 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues16False5.103 ns
DictionaryTests<Object>Dictionary_EnumerateValues16False54.844 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues16False5.045 ns
DictionaryTests<String>Dictionary_EnumerateValues16False55.299 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues16False5.048 ns
DictionaryTests<Guid>Dictionary_Construct16True276.233 ns
DictionaryTests<Guid>FrozenDictionary_Construct16True2,480.023 ns
DictionaryTests<Int32>Dictionary_Construct16True247.043 ns
DictionaryTests<Int32>FrozenDictionary_Construct16True2,679.061 ns
DictionaryTests<Object>Dictionary_Construct16True401.734 ns
DictionaryTests<Object>FrozenDictionary_Construct16True2,782.085 ns
DictionaryTests<String>Dictionary_Construct16True360.930 ns
DictionaryTests<String>FrozenDictionary_Construct16True1,385.360 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found16True116.494 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found16True124.232 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found16True87.759 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found16True50.979 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found16True207.784 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found16True209.461 ns
DictionaryTests<String>Dictionary_TryGetValue_Found16True268.939 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found16True142.170 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound16True99.116 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound16True81.180 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound16True72.956 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound16True53.239 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound16True163.937 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound16True144.715 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound16True178.219 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound16True109.456 ns
DictionaryTests<Guid>Dictionary_Enumerate16True58.001 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate16True30.898 ns
DictionaryTests<Int32>Dictionary_Enumerate16True49.878 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate16True27.879 ns
DictionaryTests<Object>Dictionary_Enumerate16True89.847 ns
DictionaryTests<Object>FrozenDictionary_Enumerate16True74.568 ns
DictionaryTests<String>Dictionary_Enumerate16True89.347 ns
DictionaryTests<String>FrozenDictionary_Enumerate16True73.377 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys16True48.112 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys16True5.259 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys16True36.038 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys16True5.016 ns
DictionaryTests<Object>Dictionary_EnumerateKeys16True74.331 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys16True19.002 ns
DictionaryTests<String>Dictionary_EnumerateKeys16True73.520 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys16True18.961 ns
DictionaryTests<Guid>Dictionary_EnumerateValues16True39.389 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues16True5.432 ns
DictionaryTests<Int32>Dictionary_EnumerateValues16True37.072 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues16True4.999 ns
DictionaryTests<Object>Dictionary_EnumerateValues16True54.973 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues16True5.033 ns
DictionaryTests<String>Dictionary_EnumerateValues16True54.496 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues16True5.029 ns
DictionaryTests<Guid>Dictionary_Construct1024False20,251.116 ns
DictionaryTests<Guid>FrozenDictionary_Construct1024False165,973.254 ns
DictionaryTests<Int32>Dictionary_Construct1024False16,858.288 ns
DictionaryTests<Int32>FrozenDictionary_Construct1024False149,391.455 ns
DictionaryTests<Object>Dictionary_Construct1024False26,154.444 ns
DictionaryTests<Object>FrozenDictionary_Construct1024False174,545.846 ns
DictionaryTests<String>Dictionary_Construct1024False36,381.752 ns
DictionaryTests<String>FrozenDictionary_Construct1024False200,478.010 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found1024False16,232.339 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found1024False14,450.573 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found1024False10,903.514 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found1024False9,294.073 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found1024False24,834.107 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found1024False27,807.133 ns
DictionaryTests<String>Dictionary_TryGetValue_Found1024False43,093.660 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found1024False39,181.201 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound1024False13,773.527 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound1024False8,281.616 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound1024False10,252.792 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound1024False6,199.940 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound1024False20,427.130 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound1024False15,131.280 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound1024False26,598.257 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound1024False24,474.515 ns
DictionaryTests<Guid>Dictionary_Enumerate1024False3,440.145 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate1024False1,689.686 ns
DictionaryTests<Int32>Dictionary_Enumerate1024False2,520.821 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate1024False1,498.062 ns
DictionaryTests<Object>Dictionary_Enumerate1024False4,408.578 ns
DictionaryTests<Object>FrozenDictionary_Enumerate1024False3,689.598 ns
DictionaryTests<String>Dictionary_Enumerate1024False4,398.440 ns
DictionaryTests<String>FrozenDictionary_Enumerate1024False3,636.864 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys1024False2,359.947 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys1024False261.473 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys1024False2,033.189 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys1024False253.029 ns
DictionaryTests<Object>Dictionary_EnumerateKeys1024False3,742.122 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys1024False1,226.121 ns
DictionaryTests<String>Dictionary_EnumerateKeys1024False3,693.788 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys1024False1,222.912 ns
DictionaryTests<Guid>Dictionary_EnumerateValues1024False2,144.243 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues1024False260.725 ns
DictionaryTests<Int32>Dictionary_EnumerateValues1024False2,039.401 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues1024False255.137 ns
DictionaryTests<Object>Dictionary_EnumerateValues1024False2,900.210 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues1024False254.610 ns
DictionaryTests<String>Dictionary_EnumerateValues1024False2,924.998 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues1024False252.582 ns
DictionaryTests<Guid>Dictionary_Construct1024True17,653.020 ns
DictionaryTests<Guid>FrozenDictionary_Construct1024True165,900.270 ns
DictionaryTests<Int32>Dictionary_Construct1024True15,324.775 ns
DictionaryTests<Int32>FrozenDictionary_Construct1024True145,562.184 ns
DictionaryTests<Object>Dictionary_Construct1024True22,357.810 ns
DictionaryTests<Object>FrozenDictionary_Construct1024True164,976.669 ns
DictionaryTests<String>Dictionary_Construct1024True24,584.158 ns
DictionaryTests<String>FrozenDictionary_Construct1024True713,506.562 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found1024True10,972.369 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found1024True10,677.337 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found1024True6,061.961 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found1024True4,568.878 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found1024True15,521.277 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found1024True15,039.072 ns
DictionaryTests<String>Dictionary_TryGetValue_Found1024True21,252.949 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found1024True19,273.593 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound1024True10,827.871 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound1024True6,294.780 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound1024True8,168.398 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound1024True4,152.874 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound1024True14,748.903 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound1024True9,459.935 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound1024True20,727.839 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound1024True11,941.794 ns
DictionaryTests<Guid>Dictionary_Enumerate1024True3,355.945 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate1024True1,676.260 ns
DictionaryTests<Int32>Dictionary_Enumerate1024True2,501.281 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate1024True1,488.353 ns
DictionaryTests<Object>Dictionary_Enumerate1024True4,433.532 ns
DictionaryTests<Object>FrozenDictionary_Enumerate1024True3,662.125 ns
DictionaryTests<String>Dictionary_Enumerate1024True4,485.860 ns
DictionaryTests<String>FrozenDictionary_Enumerate1024True3,652.590 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys1024True2,351.717 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys1024True262.712 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys1024True2,019.313 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys1024True252.664 ns
DictionaryTests<Object>Dictionary_EnumerateKeys1024True3,673.043 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys1024True1,224.610 ns
DictionaryTests<String>Dictionary_EnumerateKeys1024True3,748.785 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys1024True1,228.282 ns
DictionaryTests<Guid>Dictionary_EnumerateValues1024True2,080.066 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues1024True255.457 ns
DictionaryTests<Int32>Dictionary_EnumerateValues1024True2,018.673 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues1024True255.488 ns
DictionaryTests<Object>Dictionary_EnumerateValues1024True2,872.265 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues1024True255.872 ns
DictionaryTests<String>Dictionary_EnumerateValues1024True2,895.610 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues1024True254.709 ns
Author:stephentoub
Assignees:-
Labels:

area-System.Collections

Milestone:8.0.0

@ghostghost assigned stephentoubNov 2, 2022
@geeknoid

Copy link
Copy Markdown
Member

The read benchmarks look pretty good, but the construction phase is definitely on the slow side. I never really measure the construction time since for my scenarios I didn't care so much. I do think adding some notion of "intensity" in the constructor to let the caller decide how much time to spend optimizing would be desirable.

It would be interesting in particular to see what the read perf is if the implementation doesn't perform any optimization at all. It would likely still be faster than a normal Dictionary/HashSet when using the default comparers, but not by much. But if that case is still a bit faster and construction time is on par with normal Dictionary/HashSet, then that says that any scenario that needs a readonly dictionary or set should be using the frozen ones by default since it would be faster.

Without the ability to tune how much time is spent constructing, then you have to be careful where you use the frozen collections and make sure the cost/benefit will be worth it.

@geeknoid

geeknoid commented Nov 2, 2022

Copy link
Copy Markdown
Member

Some thoughts for the future:

  • Dictionaries and sets with low cardinality are common. We should look at adding specialized implementation for 1 and 2 entry collections. This saves memory and speeds things up.

  • I had on my todo list to create specialized two character string comparers to optimize that case. For small collections, often the single char comparer is enough and works well. But when the collection grows, you run out of single characters to compare, so the code falls back to two.

  • We discussed in the review that the majority use case for this kind of collection is where the data comes dynamically into the process. This says that doing the optimization work for the collections in a source generator is not likely to be used frequently. However, what I think might be useful is the ability to serialize/deserialize the internal state for the frozen collections in a portable way. This would let me distribute this serialized state as config, and thus forego the construction overhead. So instead of putting a bunch of key/value pairs in my config and having to optimize those in each of my service's processes, I could optimize the key/value pairs offline and push the resulting state to the processes.

stephentouband others added 2 commits November 2, 2022 17:45
…y compile
Co-authored-by: Martin Taillefer <mataille@microsoft.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>

@geeknoidgeeknoid left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This looks great.

@krwq

krwq commented Nov 4, 2022

Copy link
Copy Markdown
Member

I'd be really interested to see how integration of this feature would look like with System.Text.Json (both JsonSerializerOptions.Converters and JsonTypeInfo.Properties are freezing values at one point)

krwq
krwq approved these changes Nov 4, 2022

@krwqkrwq left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I've skimmed all of the files and overall looks good. I'm really impressed in the amount of heuristics done here to make this super optimized. Thank you!

@stephentoub
stephentoub merged commit 6c5a440 into dotnet:mainNov 4, 2022
@stephentoub
stephentoub deleted the frozencollections branch November 4, 2022 19:49
@ghostghost locked as resolved and limited conversation to collaborators Dec 5, 2022
@jeffhandleyjeffhandley added the blog-candidate Completed PRs that are candidate topics for blog post coverage label Mar 14, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Collectionsblog-candidateCompleted PRs that are candidate topics for blog post coveragenew-api-needs-documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[API Proposal]: Provide optimized read-only collections

4 participants

@stephentoub@geeknoid@krwq@jeffhandley
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add FrozenDictionary/Set - #77799

Merged
stephentoub merged 3 commits into
dotnet:mainfrom
stephentoub:frozencollections
Nov 4, 2022
Merged

Add FrozenDictionary/Set#77799
stephentoub merged 3 commits into
dotnet:mainfrom
stephentoub:frozencollections

Conversation

@stephentoub

@stephentoubstephentoub commented Nov 2, 2022

Copy link
Copy Markdown
Member

Fixes#67209

The first commit imports @geeknoid's source, updated to compile in System.Collections.Immutable.dll.

The second commit overhauls the APIs and adds tests. Overhauling the APIs also required lots of surgery on various aspects of the implementation.

This includes a bunch of updates to our shared collections tests, which we've apparently not used in earnest with read-only collections, as a bunch of mutating tests were trying to run even when IsReadOnly was set to false.

There's still more work required here. Subsequent to this PR, we should:

  • Investigate ways to make construction faster.
  • Investigate ways to make lookups faster.
  • Investigate what exact special-cased implementations we want to ship.

(@geeknoid, your help here would be appreciated. Best case numbers look very good, e.g. the example benchmarks you previously created, but others don't look nearly as good. I vetted some of the worst numbers against the original implementation, and they're similar.)

Right now, for each of dictionary/set, there are custom implementations for:

  • Int32 keys with the default comparer
  • Value type keys with the default comparer
  • String keys with default/ordinal/ordinal-ignore-case comparers. Here there are two: one that tries to bucket a small number of strings by length, and one that tries to avoid reading/hashing the full strings by selecting the smallest portion that's unique enough.
  • A general fallback implementation that's used for everything else.

We can explore other data structures, other optimizations, etc., all behind the same veneer. Some of the existing implementations might not be worthwhile, too. For some situations, we might even want an implementation that just wraps Dictionary<,> along with dedicated keys/values arrays.

But the first step is getting the APIs merged to enable others to experiment.

Some benchmarks
[InProcess][GenericTypeArguments(typeof(string))][GenericTypeArguments(typeof(int))][GenericTypeArguments(typeof(object))][GenericTypeArguments(typeof(Guid))]publicclassDictionaryTests<TKey>{privateTKey[]_keys;privateTKey[]_nonExistentKeys;privateDictionary<TKey,long>_dictionary;privateFrozenDictionary<TKey,long>_frozenDictionary;[Params(1,16,1024)]publicintCount{get;set;}[Params(false,true)]publicboolDefaultComparer{get;set;}privatestaticIEqualityComparer<TKey>GetComparer(booldefaultComparer)=>defaultComparer?EqualityComparer<TKey>.Default:NonDefaultEqualityComparer<TKey>.Instance;privateunsafestaticTRandGetRandomValue<TRand>(Randomrand){if(typeof(TRand)==typeof(string)){Span<char>span=stackallocchar[rand.Next(5,20)];for(inti=0;i<span.Length;i++){span[i]=(char)('a'+rand.Next(0,26));}return(TRand)(object)span.ToString();}if(typeof(TRand)==typeof(object)){return(TRand)newobject();}if(typeof(TRand)==typeof(Guid)){Int128value=newInt128((ulong)rand.NextInt64(),(ulong)rand.NextInt64());return(TRand)(object)(*(Guid*)(&value));}if(typeof(TRand)==typeof(int)){return(TRand)(object)rand.Next();}if(typeof(TRand)==typeof(long)){return(TRand)(object)rand.NextInt64();}thrownewException("Unknown type");}[GlobalSetup]publicvoidSetup(){varrand=newRandom(42);_dictionary=newDictionary<TKey,long>(GetComparer(DefaultComparer));for(intj=0;j<Count;j++){_dictionary[GetRandomValue<TKey>(rand)]=GetRandomValue<long>(rand);}_frozenDictionary=_dictionary.ToFrozenDictionary(GetComparer(DefaultComparer));_keys=_dictionary.Keys.ToArray();if(typeof(TKey)==typeof(string)){_keys=(TKey[])(object)Array.ConvertAll((string[])(object)_keys,string.Copy);}_nonExistentKeys=newTKey[Count];for(inti=0;i<Count;i++){TKeykey;while(_dictionary.ContainsKey(key=GetRandomValue<TKey>(rand)));_nonExistentKeys[i]=key;}}[Benchmark]publicDictionary<TKey,long>Dictionary_Construct(){return_keys.ToDictionary((TKeyk)=>k,(TKeyk)=>0L,GetComparer(DefaultComparer));}[Benchmark]publicFrozenDictionary<TKey,long>FrozenDictionary_Construct(){return_keys.ToFrozenDictionary((TKeyk)=>k,(TKeyk)=>0L,GetComparer(DefaultComparer));}[Benchmark]publicboolDictionary_TryGetValue_Found(){boolallFound=true;TKey[]keys=_keys;foreach(TKeykeyinkeys){longvalue;allFound&=_dictionary.TryGetValue(key,outvalue);}returnallFound;}[Benchmark]publicboolDictionary_TryGetValue_NotFound(){boolanyFound=false;TKey[]nonExistentKeys=_nonExistentKeys;foreach(TKeykeyinnonExistentKeys){longvalue;anyFound|=_dictionary.TryGetValue(key,outvalue);}returnanyFound;}[Benchmark]publicboolFrozenDictionary_TryGetValue_Found(){boolallFound=true;TKey[]keys=_keys;foreach(TKeykeyinkeys){longvalue;allFound&=_frozenDictionary.TryGetValue(key,outvalue);}returnallFound;}[Benchmark]publicboolFrozenDictionary_TryGetValue_NotFound(){boolanyFound=false;TKey[]nonExistentKeys=_nonExistentKeys;foreach(TKeykeyinnonExistentKeys){longvalue;anyFound|=_frozenDictionary.TryGetValue(key,outvalue);}returnanyFound;}[Benchmark]publicintDictionary_Enumerate(){intcount=0;foreach(KeyValuePair<TKey,long>itemin_dictionary){_=item;count++;}returncount;}[Benchmark]publicintDictionary_EnumerateKeys(){intcount=0;foreach(TKeykeyin_dictionary.Keys){_=key;count++;}returncount;}[Benchmark]publiclongDictionary_EnumerateValues(){intcount=0;foreach(longvaluein_dictionary.Values){_=value;count++;}returncount;}[Benchmark]publicintFrozenDictionary_Enumerate(){intcount=0;foreach(KeyValuePair<TKey,long>itemin_frozenDictionary){_=item;count++;}returncount;}[Benchmark]publicintFrozenDictionary_EnumerateKeys(){intcount=0;foreach(TKeykeyin_frozenDictionary.Keys){count++;}returncount;}[Benchmark]publicintFrozenDictionary_EnumerateValues(){intcount=0;foreach(longvaluein_frozenDictionary.Values){count++;}returncount;}}internalsealedpartialclassNonDefaultEqualityComparer<T>:EqualityComparer<T>{publicstaticNonDefaultEqualityComparer<T>Instance{get;}=newNonDefaultEqualityComparer<T>();publicoverrideboolEquals(Tx,Ty)=>Default.Equals(x,y);publicoverrideintGetHashCode(Tobj)=>Default.GetHashCode(obj);}
TypeMethodCountDefaultComparerMean
DictionaryTests<Guid>Dictionary_Construct16False308.917 ns
DictionaryTests<Guid>FrozenDictionary_Construct16False2,546.966 ns
DictionaryTests<Int32>Dictionary_Construct16False256.919 ns
DictionaryTests<Int32>FrozenDictionary_Construct16False2,735.513 ns
DictionaryTests<Object>Dictionary_Construct16False459.599 ns
DictionaryTests<Object>FrozenDictionary_Construct16False2,990.372 ns
DictionaryTests<String>Dictionary_Construct16False574.025 ns
DictionaryTests<String>FrozenDictionary_Construct16False3,085.790 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found16False195.613 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found16False180.628 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found16False127.828 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found16False120.595 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found16False371.343 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found16False384.100 ns
DictionaryTests<String>Dictionary_TryGetValue_Found16False559.943 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found16False531.615 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound16False149.167 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound16False120.109 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound16False109.596 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound16False91.319 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound16False247.097 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound16False231.592 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound16False356.568 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound16False334.678 ns
DictionaryTests<Guid>Dictionary_Enumerate16False61.120 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate16False27.948 ns
DictionaryTests<Int32>Dictionary_Enumerate16False50.372 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate16False27.162 ns
DictionaryTests<Object>Dictionary_Enumerate16False89.827 ns
DictionaryTests<Object>FrozenDictionary_Enumerate16False74.392 ns
DictionaryTests<String>Dictionary_Enumerate16False89.446 ns
DictionaryTests<String>FrozenDictionary_Enumerate16False74.626 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys16False49.057 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys16False4.847 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys16False37.000 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys16False5.108 ns
DictionaryTests<Object>Dictionary_EnumerateKeys16False74.497 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys16False18.488 ns
DictionaryTests<String>Dictionary_EnumerateKeys16False74.906 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys16False18.571 ns
DictionaryTests<Guid>Dictionary_EnumerateValues16False38.145 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues16False5.054 ns
DictionaryTests<Int32>Dictionary_EnumerateValues16False36.834 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues16False5.103 ns
DictionaryTests<Object>Dictionary_EnumerateValues16False54.844 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues16False5.045 ns
DictionaryTests<String>Dictionary_EnumerateValues16False55.299 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues16False5.048 ns
DictionaryTests<Guid>Dictionary_Construct16True276.233 ns
DictionaryTests<Guid>FrozenDictionary_Construct16True2,480.023 ns
DictionaryTests<Int32>Dictionary_Construct16True247.043 ns
DictionaryTests<Int32>FrozenDictionary_Construct16True2,679.061 ns
DictionaryTests<Object>Dictionary_Construct16True401.734 ns
DictionaryTests<Object>FrozenDictionary_Construct16True2,782.085 ns
DictionaryTests<String>Dictionary_Construct16True360.930 ns
DictionaryTests<String>FrozenDictionary_Construct16True1,385.360 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found16True116.494 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found16True124.232 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found16True87.759 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found16True50.979 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found16True207.784 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found16True209.461 ns
DictionaryTests<String>Dictionary_TryGetValue_Found16True268.939 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found16True142.170 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound16True99.116 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound16True81.180 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound16True72.956 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound16True53.239 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound16True163.937 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound16True144.715 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound16True178.219 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound16True109.456 ns
DictionaryTests<Guid>Dictionary_Enumerate16True58.001 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate16True30.898 ns
DictionaryTests<Int32>Dictionary_Enumerate16True49.878 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate16True27.879 ns
DictionaryTests<Object>Dictionary_Enumerate16True89.847 ns
DictionaryTests<Object>FrozenDictionary_Enumerate16True74.568 ns
DictionaryTests<String>Dictionary_Enumerate16True89.347 ns
DictionaryTests<String>FrozenDictionary_Enumerate16True73.377 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys16True48.112 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys16True5.259 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys16True36.038 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys16True5.016 ns
DictionaryTests<Object>Dictionary_EnumerateKeys16True74.331 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys16True19.002 ns
DictionaryTests<String>Dictionary_EnumerateKeys16True73.520 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys16True18.961 ns
DictionaryTests<Guid>Dictionary_EnumerateValues16True39.389 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues16True5.432 ns
DictionaryTests<Int32>Dictionary_EnumerateValues16True37.072 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues16True4.999 ns
DictionaryTests<Object>Dictionary_EnumerateValues16True54.973 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues16True5.033 ns
DictionaryTests<String>Dictionary_EnumerateValues16True54.496 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues16True5.029 ns
DictionaryTests<Guid>Dictionary_Construct1024False20,251.116 ns
DictionaryTests<Guid>FrozenDictionary_Construct1024False165,973.254 ns
DictionaryTests<Int32>Dictionary_Construct1024False16,858.288 ns
DictionaryTests<Int32>FrozenDictionary_Construct1024False149,391.455 ns
DictionaryTests<Object>Dictionary_Construct1024False26,154.444 ns
DictionaryTests<Object>FrozenDictionary_Construct1024False174,545.846 ns
DictionaryTests<String>Dictionary_Construct1024False36,381.752 ns
DictionaryTests<String>FrozenDictionary_Construct1024False200,478.010 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found1024False16,232.339 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found1024False14,450.573 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found1024False10,903.514 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found1024False9,294.073 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found1024False24,834.107 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found1024False27,807.133 ns
DictionaryTests<String>Dictionary_TryGetValue_Found1024False43,093.660 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found1024False39,181.201 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound1024False13,773.527 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound1024False8,281.616 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound1024False10,252.792 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound1024False6,199.940 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound1024False20,427.130 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound1024False15,131.280 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound1024False26,598.257 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound1024False24,474.515 ns
DictionaryTests<Guid>Dictionary_Enumerate1024False3,440.145 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate1024False1,689.686 ns
DictionaryTests<Int32>Dictionary_Enumerate1024False2,520.821 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate1024False1,498.062 ns
DictionaryTests<Object>Dictionary_Enumerate1024False4,408.578 ns
DictionaryTests<Object>FrozenDictionary_Enumerate1024False3,689.598 ns
DictionaryTests<String>Dictionary_Enumerate1024False4,398.440 ns
DictionaryTests<String>FrozenDictionary_Enumerate1024False3,636.864 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys1024False2,359.947 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys1024False261.473 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys1024False2,033.189 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys1024False253.029 ns
DictionaryTests<Object>Dictionary_EnumerateKeys1024False3,742.122 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys1024False1,226.121 ns
DictionaryTests<String>Dictionary_EnumerateKeys1024False3,693.788 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys1024False1,222.912 ns
DictionaryTests<Guid>Dictionary_EnumerateValues1024False2,144.243 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues1024False260.725 ns
DictionaryTests<Int32>Dictionary_EnumerateValues1024False2,039.401 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues1024False255.137 ns
DictionaryTests<Object>Dictionary_EnumerateValues1024False2,900.210 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues1024False254.610 ns
DictionaryTests<String>Dictionary_EnumerateValues1024False2,924.998 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues1024False252.582 ns
DictionaryTests<Guid>Dictionary_Construct1024True17,653.020 ns
DictionaryTests<Guid>FrozenDictionary_Construct1024True165,900.270 ns
DictionaryTests<Int32>Dictionary_Construct1024True15,324.775 ns
DictionaryTests<Int32>FrozenDictionary_Construct1024True145,562.184 ns
DictionaryTests<Object>Dictionary_Construct1024True22,357.810 ns
DictionaryTests<Object>FrozenDictionary_Construct1024True164,976.669 ns
DictionaryTests<String>Dictionary_Construct1024True24,584.158 ns
DictionaryTests<String>FrozenDictionary_Construct1024True713,506.562 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found1024True10,972.369 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found1024True10,677.337 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found1024True6,061.961 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found1024True4,568.878 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found1024True15,521.277 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found1024True15,039.072 ns
DictionaryTests<String>Dictionary_TryGetValue_Found1024True21,252.949 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found1024True19,273.593 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound1024True10,827.871 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound1024True6,294.780 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound1024True8,168.398 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound1024True4,152.874 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound1024True14,748.903 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound1024True9,459.935 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound1024True20,727.839 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound1024True11,941.794 ns
DictionaryTests<Guid>Dictionary_Enumerate1024True3,355.945 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate1024True1,676.260 ns
DictionaryTests<Int32>Dictionary_Enumerate1024True2,501.281 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate1024True1,488.353 ns
DictionaryTests<Object>Dictionary_Enumerate1024True4,433.532 ns
DictionaryTests<Object>FrozenDictionary_Enumerate1024True3,662.125 ns
DictionaryTests<String>Dictionary_Enumerate1024True4,485.860 ns
DictionaryTests<String>FrozenDictionary_Enumerate1024True3,652.590 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys1024True2,351.717 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys1024True262.712 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys1024True2,019.313 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys1024True252.664 ns
DictionaryTests<Object>Dictionary_EnumerateKeys1024True3,673.043 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys1024True1,224.610 ns
DictionaryTests<String>Dictionary_EnumerateKeys1024True3,748.785 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys1024True1,228.282 ns
DictionaryTests<Guid>Dictionary_EnumerateValues1024True2,080.066 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues1024True255.457 ns
DictionaryTests<Int32>Dictionary_EnumerateValues1024True2,018.673 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues1024True255.488 ns
DictionaryTests<Object>Dictionary_EnumerateValues1024True2,872.265 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues1024True255.872 ns
DictionaryTests<String>Dictionary_EnumerateValues1024True2,895.610 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues1024True254.709 ns

@ghost

ghost commented Nov 2, 2022

Copy link
Copy Markdown

Note regarding the new-api-needs-documentation label:

This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, to please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change.

@ghost

ghost commented Nov 2, 2022

Copy link
Copy Markdown

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

Issue Details

The first commit imports @geeknoid's source, updated to compile in System.Collections.Immutable.dll.

The second commit overhauls the APIs and adds tests.

This includes a bunch of updates to our shared collections tests, which we've apparently not used in earnest with read-only collections, as a bunch of mutating tests were trying to run even when IsReadOnly was set to false.

There's still more work required here. Subsequent to this PR, we should:

  • Investigate ways to make construction faster.
  • Investigate ways to make lookups faster.
  • Investigate what exact special-cased implementations we want to ship.

(@geeknoid, your help here would be appreciated. Best case numbers look very good, e.g. the example benchmarks you previously created, but others don't look nearly as good. I vetted some of the worst numbers against the original implementation, and they're similar.)

Right now, for each of dictionary/set, there are custom implementations for:

  • Int32 keys with the default comparer
  • Value type keys with the default comparer
  • String keys with default/ordinal/ordinal-ignore-case comparers. Here there are two: one that tries to bucket a small number of strings by length, and one that tries to avoid reading/hashing the full strings by selecting the smallest portion that's unique enough.
  • A general fallback implementation that's used for everything else.

We can explore other data structures, other optimizations, etc., all behind the same veneer. Some of the existing implementations might not be worthwhile, too. For some situations, we might even want an implementation that just wraps Dictionary<,> along with dedicated keys/values arrays.

But the first step is getting the APIs merged to enable others to experiment.

Some benchmarks
[InProcess][GenericTypeArguments(typeof(string))][GenericTypeArguments(typeof(int))][GenericTypeArguments(typeof(object))][GenericTypeArguments(typeof(Guid))]publicclassDictionaryTests<TKey>{privateTKey[]_keys;privateTKey[]_nonExistentKeys;privateDictionary<TKey,long>_dictionary;privateFrozenDictionary<TKey,long>_frozenDictionary;[Params(1,16,1024)]publicintCount{get;set;}[Params(false,true)]publicboolDefaultComparer{get;set;}privatestaticIEqualityComparer<TKey>GetComparer(booldefaultComparer)=>defaultComparer?EqualityComparer<TKey>.Default:NonDefaultEqualityComparer<TKey>.Instance;privateunsafestaticTRandGetRandomValue<TRand>(Randomrand){if(typeof(TRand)==typeof(string)){Span<char>span=stackallocchar[rand.Next(5,20)];for(inti=0;i<span.Length;i++){span[i]=(char)('a'+rand.Next(0,26));}return(TRand)(object)span.ToString();}if(typeof(TRand)==typeof(object)){return(TRand)newobject();}if(typeof(TRand)==typeof(Guid)){Int128value=newInt128((ulong)rand.NextInt64(),(ulong)rand.NextInt64());return(TRand)(object)(*(Guid*)(&value));}if(typeof(TRand)==typeof(int)){return(TRand)(object)rand.Next();}if(typeof(TRand)==typeof(long)){return(TRand)(object)rand.NextInt64();}thrownewException("Unknown type");}[GlobalSetup]publicvoidSetup(){varrand=newRandom(42);_dictionary=newDictionary<TKey,long>(GetComparer(DefaultComparer));for(intj=0;j<Count;j++){_dictionary[GetRandomValue<TKey>(rand)]=GetRandomValue<long>(rand);}_frozenDictionary=_dictionary.ToFrozenDictionary(GetComparer(DefaultComparer));_keys=_dictionary.Keys.ToArray();if(typeof(TKey)==typeof(string)){_keys=(TKey[])(object)Array.ConvertAll((string[])(object)_keys,string.Copy);}_nonExistentKeys=newTKey[Count];for(inti=0;i<Count;i++){TKeykey;while(_dictionary.ContainsKey(key=GetRandomValue<TKey>(rand)));_nonExistentKeys[i]=key;}}[Benchmark]publicDictionary<TKey,long>Dictionary_Construct(){return_keys.ToDictionary((TKeyk)=>k,(TKeyk)=>0L,GetComparer(DefaultComparer));}[Benchmark]publicFrozenDictionary<TKey,long>FrozenDictionary_Construct(){return_keys.ToFrozenDictionary((TKeyk)=>k,(TKeyk)=>0L,GetComparer(DefaultComparer));}[Benchmark]publicboolDictionary_TryGetValue_Found(){boolallFound=true;TKey[]keys=_keys;foreach(TKeykeyinkeys){longvalue;allFound&=_dictionary.TryGetValue(key,outvalue);}returnallFound;}[Benchmark]publicboolDictionary_TryGetValue_NotFound(){boolanyFound=false;TKey[]nonExistentKeys=_nonExistentKeys;foreach(TKeykeyinnonExistentKeys){longvalue;anyFound|=_dictionary.TryGetValue(key,outvalue);}returnanyFound;}[Benchmark]publicboolFrozenDictionary_TryGetValue_Found(){boolallFound=true;TKey[]keys=_keys;foreach(TKeykeyinkeys){longvalue;allFound&=_frozenDictionary.TryGetValue(key,outvalue);}returnallFound;}[Benchmark]publicboolFrozenDictionary_TryGetValue_NotFound(){boolanyFound=false;TKey[]nonExistentKeys=_nonExistentKeys;foreach(TKeykeyinnonExistentKeys){longvalue;anyFound|=_frozenDictionary.TryGetValue(key,outvalue);}returnanyFound;}[Benchmark]publicintDictionary_Enumerate(){intcount=0;foreach(KeyValuePair<TKey,long>itemin_dictionary){_=item;count++;}returncount;}[Benchmark]publicintDictionary_EnumerateKeys(){intcount=0;foreach(TKeykeyin_dictionary.Keys){_=key;count++;}returncount;}[Benchmark]publiclongDictionary_EnumerateValues(){intcount=0;foreach(longvaluein_dictionary.Values){_=value;count++;}returncount;}[Benchmark]publicintFrozenDictionary_Enumerate(){intcount=0;foreach(KeyValuePair<TKey,long>itemin_frozenDictionary){_=item;count++;}returncount;}[Benchmark]publicintFrozenDictionary_EnumerateKeys(){intcount=0;foreach(TKeykeyin_frozenDictionary.Keys){count++;}returncount;}[Benchmark]publicintFrozenDictionary_EnumerateValues(){intcount=0;foreach(longvaluein_frozenDictionary.Values){count++;}returncount;}}internalsealedpartialclassNonDefaultEqualityComparer<T>:EqualityComparer<T>{publicstaticNonDefaultEqualityComparer<T>Instance{get;}=newNonDefaultEqualityComparer<T>();publicoverrideboolEquals(Tx,Ty)=>Default.Equals(x,y);publicoverrideintGetHashCode(Tobj)=>Default.GetHashCode(obj);}
TypeMethodCountDefaultComparerMean
DictionaryTests<Guid>Dictionary_Construct16False308.917 ns
DictionaryTests<Guid>FrozenDictionary_Construct16False2,546.966 ns
DictionaryTests<Int32>Dictionary_Construct16False256.919 ns
DictionaryTests<Int32>FrozenDictionary_Construct16False2,735.513 ns
DictionaryTests<Object>Dictionary_Construct16False459.599 ns
DictionaryTests<Object>FrozenDictionary_Construct16False2,990.372 ns
DictionaryTests<String>Dictionary_Construct16False574.025 ns
DictionaryTests<String>FrozenDictionary_Construct16False3,085.790 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found16False195.613 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found16False180.628 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found16False127.828 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found16False120.595 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found16False371.343 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found16False384.100 ns
DictionaryTests<String>Dictionary_TryGetValue_Found16False559.943 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found16False531.615 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound16False149.167 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound16False120.109 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound16False109.596 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound16False91.319 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound16False247.097 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound16False231.592 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound16False356.568 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound16False334.678 ns
DictionaryTests<Guid>Dictionary_Enumerate16False61.120 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate16False27.948 ns
DictionaryTests<Int32>Dictionary_Enumerate16False50.372 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate16False27.162 ns
DictionaryTests<Object>Dictionary_Enumerate16False89.827 ns
DictionaryTests<Object>FrozenDictionary_Enumerate16False74.392 ns
DictionaryTests<String>Dictionary_Enumerate16False89.446 ns
DictionaryTests<String>FrozenDictionary_Enumerate16False74.626 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys16False49.057 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys16False4.847 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys16False37.000 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys16False5.108 ns
DictionaryTests<Object>Dictionary_EnumerateKeys16False74.497 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys16False18.488 ns
DictionaryTests<String>Dictionary_EnumerateKeys16False74.906 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys16False18.571 ns
DictionaryTests<Guid>Dictionary_EnumerateValues16False38.145 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues16False5.054 ns
DictionaryTests<Int32>Dictionary_EnumerateValues16False36.834 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues16False5.103 ns
DictionaryTests<Object>Dictionary_EnumerateValues16False54.844 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues16False5.045 ns
DictionaryTests<String>Dictionary_EnumerateValues16False55.299 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues16False5.048 ns
DictionaryTests<Guid>Dictionary_Construct16True276.233 ns
DictionaryTests<Guid>FrozenDictionary_Construct16True2,480.023 ns
DictionaryTests<Int32>Dictionary_Construct16True247.043 ns
DictionaryTests<Int32>FrozenDictionary_Construct16True2,679.061 ns
DictionaryTests<Object>Dictionary_Construct16True401.734 ns
DictionaryTests<Object>FrozenDictionary_Construct16True2,782.085 ns
DictionaryTests<String>Dictionary_Construct16True360.930 ns
DictionaryTests<String>FrozenDictionary_Construct16True1,385.360 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found16True116.494 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found16True124.232 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found16True87.759 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found16True50.979 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found16True207.784 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found16True209.461 ns
DictionaryTests<String>Dictionary_TryGetValue_Found16True268.939 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found16True142.170 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound16True99.116 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound16True81.180 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound16True72.956 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound16True53.239 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound16True163.937 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound16True144.715 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound16True178.219 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound16True109.456 ns
DictionaryTests<Guid>Dictionary_Enumerate16True58.001 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate16True30.898 ns
DictionaryTests<Int32>Dictionary_Enumerate16True49.878 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate16True27.879 ns
DictionaryTests<Object>Dictionary_Enumerate16True89.847 ns
DictionaryTests<Object>FrozenDictionary_Enumerate16True74.568 ns
DictionaryTests<String>Dictionary_Enumerate16True89.347 ns
DictionaryTests<String>FrozenDictionary_Enumerate16True73.377 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys16True48.112 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys16True5.259 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys16True36.038 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys16True5.016 ns
DictionaryTests<Object>Dictionary_EnumerateKeys16True74.331 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys16True19.002 ns
DictionaryTests<String>Dictionary_EnumerateKeys16True73.520 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys16True18.961 ns
DictionaryTests<Guid>Dictionary_EnumerateValues16True39.389 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues16True5.432 ns
DictionaryTests<Int32>Dictionary_EnumerateValues16True37.072 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues16True4.999 ns
DictionaryTests<Object>Dictionary_EnumerateValues16True54.973 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues16True5.033 ns
DictionaryTests<String>Dictionary_EnumerateValues16True54.496 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues16True5.029 ns
DictionaryTests<Guid>Dictionary_Construct1024False20,251.116 ns
DictionaryTests<Guid>FrozenDictionary_Construct1024False165,973.254 ns
DictionaryTests<Int32>Dictionary_Construct1024False16,858.288 ns
DictionaryTests<Int32>FrozenDictionary_Construct1024False149,391.455 ns
DictionaryTests<Object>Dictionary_Construct1024False26,154.444 ns
DictionaryTests<Object>FrozenDictionary_Construct1024False174,545.846 ns
DictionaryTests<String>Dictionary_Construct1024False36,381.752 ns
DictionaryTests<String>FrozenDictionary_Construct1024False200,478.010 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found1024False16,232.339 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found1024False14,450.573 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found1024False10,903.514 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found1024False9,294.073 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found1024False24,834.107 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found1024False27,807.133 ns
DictionaryTests<String>Dictionary_TryGetValue_Found1024False43,093.660 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found1024False39,181.201 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound1024False13,773.527 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound1024False8,281.616 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound1024False10,252.792 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound1024False6,199.940 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound1024False20,427.130 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound1024False15,131.280 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound1024False26,598.257 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound1024False24,474.515 ns
DictionaryTests<Guid>Dictionary_Enumerate1024False3,440.145 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate1024False1,689.686 ns
DictionaryTests<Int32>Dictionary_Enumerate1024False2,520.821 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate1024False1,498.062 ns
DictionaryTests<Object>Dictionary_Enumerate1024False4,408.578 ns
DictionaryTests<Object>FrozenDictionary_Enumerate1024False3,689.598 ns
DictionaryTests<String>Dictionary_Enumerate1024False4,398.440 ns
DictionaryTests<String>FrozenDictionary_Enumerate1024False3,636.864 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys1024False2,359.947 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys1024False261.473 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys1024False2,033.189 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys1024False253.029 ns
DictionaryTests<Object>Dictionary_EnumerateKeys1024False3,742.122 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys1024False1,226.121 ns
DictionaryTests<String>Dictionary_EnumerateKeys1024False3,693.788 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys1024False1,222.912 ns
DictionaryTests<Guid>Dictionary_EnumerateValues1024False2,144.243 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues1024False260.725 ns
DictionaryTests<Int32>Dictionary_EnumerateValues1024False2,039.401 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues1024False255.137 ns
DictionaryTests<Object>Dictionary_EnumerateValues1024False2,900.210 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues1024False254.610 ns
DictionaryTests<String>Dictionary_EnumerateValues1024False2,924.998 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues1024False252.582 ns
DictionaryTests<Guid>Dictionary_Construct1024True17,653.020 ns
DictionaryTests<Guid>FrozenDictionary_Construct1024True165,900.270 ns
DictionaryTests<Int32>Dictionary_Construct1024True15,324.775 ns
DictionaryTests<Int32>FrozenDictionary_Construct1024True145,562.184 ns
DictionaryTests<Object>Dictionary_Construct1024True22,357.810 ns
DictionaryTests<Object>FrozenDictionary_Construct1024True164,976.669 ns
DictionaryTests<String>Dictionary_Construct1024True24,584.158 ns
DictionaryTests<String>FrozenDictionary_Construct1024True713,506.562 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found1024True10,972.369 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found1024True10,677.337 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found1024True6,061.961 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found1024True4,568.878 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found1024True15,521.277 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found1024True15,039.072 ns
DictionaryTests<String>Dictionary_TryGetValue_Found1024True21,252.949 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found1024True19,273.593 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound1024True10,827.871 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound1024True6,294.780 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound1024True8,168.398 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound1024True4,152.874 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound1024True14,748.903 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound1024True9,459.935 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound1024True20,727.839 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound1024True11,941.794 ns
DictionaryTests<Guid>Dictionary_Enumerate1024True3,355.945 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate1024True1,676.260 ns
DictionaryTests<Int32>Dictionary_Enumerate1024True2,501.281 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate1024True1,488.353 ns
DictionaryTests<Object>Dictionary_Enumerate1024True4,433.532 ns
DictionaryTests<Object>FrozenDictionary_Enumerate1024True3,662.125 ns
DictionaryTests<String>Dictionary_Enumerate1024True4,485.860 ns
DictionaryTests<String>FrozenDictionary_Enumerate1024True3,652.590 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys1024True2,351.717 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys1024True262.712 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys1024True2,019.313 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys1024True252.664 ns
DictionaryTests<Object>Dictionary_EnumerateKeys1024True3,673.043 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys1024True1,224.610 ns
DictionaryTests<String>Dictionary_EnumerateKeys1024True3,748.785 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys1024True1,228.282 ns
DictionaryTests<Guid>Dictionary_EnumerateValues1024True2,080.066 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues1024True255.457 ns
DictionaryTests<Int32>Dictionary_EnumerateValues1024True2,018.673 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues1024True255.488 ns
DictionaryTests<Object>Dictionary_EnumerateValues1024True2,872.265 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues1024True255.872 ns
DictionaryTests<String>Dictionary_EnumerateValues1024True2,895.610 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues1024True254.709 ns
Author:stephentoub
Assignees:-
Labels:

area-System.Collections

Milestone:8.0.0

@ghostghost assigned stephentoubNov 2, 2022
@geeknoid

Copy link
Copy Markdown
Member

The read benchmarks look pretty good, but the construction phase is definitely on the slow side. I never really measure the construction time since for my scenarios I didn't care so much. I do think adding some notion of "intensity" in the constructor to let the caller decide how much time to spend optimizing would be desirable.

It would be interesting in particular to see what the read perf is if the implementation doesn't perform any optimization at all. It would likely still be faster than a normal Dictionary/HashSet when using the default comparers, but not by much. But if that case is still a bit faster and construction time is on par with normal Dictionary/HashSet, then that says that any scenario that needs a readonly dictionary or set should be using the frozen ones by default since it would be faster.

Without the ability to tune how much time is spent constructing, then you have to be careful where you use the frozen collections and make sure the cost/benefit will be worth it.

@geeknoid

geeknoid commented Nov 2, 2022

Copy link
Copy Markdown
Member

Some thoughts for the future:

  • Dictionaries and sets with low cardinality are common. We should look at adding specialized implementation for 1 and 2 entry collections. This saves memory and speeds things up.

  • I had on my todo list to create specialized two character string comparers to optimize that case. For small collections, often the single char comparer is enough and works well. But when the collection grows, you run out of single characters to compare, so the code falls back to two.

  • We discussed in the review that the majority use case for this kind of collection is where the data comes dynamically into the process. This says that doing the optimization work for the collections in a source generator is not likely to be used frequently. However, what I think might be useful is the ability to serialize/deserialize the internal state for the frozen collections in a portable way. This would let me distribute this serialized state as config, and thus forego the construction overhead. So instead of putting a bunch of key/value pairs in my config and having to optimize those in each of my service's processes, I could optimize the key/value pairs offline and push the resulting state to the processes.

stephentouband others added 2 commits November 2, 2022 17:45
…y compile
Co-authored-by: Martin Taillefer <mataille@microsoft.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>

@geeknoidgeeknoid left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This looks great.

@krwq

krwq commented Nov 4, 2022

Copy link
Copy Markdown
Member

I'd be really interested to see how integration of this feature would look like with System.Text.Json (both JsonSerializerOptions.Converters and JsonTypeInfo.Properties are freezing values at one point)

krwq
krwq approved these changes Nov 4, 2022

@krwqkrwq left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I've skimmed all of the files and overall looks good. I'm really impressed in the amount of heuristics done here to make this super optimized. Thank you!

@stephentoub
stephentoub merged commit 6c5a440 into dotnet:mainNov 4, 2022
@stephentoub
stephentoub deleted the frozencollections branch November 4, 2022 19:49
@ghostghost locked as resolved and limited conversation to collaborators Dec 5, 2022
@jeffhandleyjeffhandley added the blog-candidate Completed PRs that are candidate topics for blog post coverage label Mar 14, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Collectionsblog-candidateCompleted PRs that are candidate topics for blog post coveragenew-api-needs-documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[API Proposal]: Provide optimized read-only collections

4 participants

@stephentoub@geeknoid@krwq@jeffhandley
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Add FrozenDictionary/Set - #77799

Merged
stephentoub merged 3 commits into
dotnet:mainfrom
stephentoub:frozencollections
Nov 4, 2022
Merged

Add FrozenDictionary/Set#77799
stephentoub merged 3 commits into
dotnet:mainfrom
stephentoub:frozencollections

Conversation

@stephentoub

@stephentoubstephentoub commented Nov 2, 2022

Copy link
Copy Markdown
Member

Fixes#67209

The first commit imports @geeknoid's source, updated to compile in System.Collections.Immutable.dll.

The second commit overhauls the APIs and adds tests. Overhauling the APIs also required lots of surgery on various aspects of the implementation.

This includes a bunch of updates to our shared collections tests, which we've apparently not used in earnest with read-only collections, as a bunch of mutating tests were trying to run even when IsReadOnly was set to false.

There's still more work required here. Subsequent to this PR, we should:

  • Investigate ways to make construction faster.
  • Investigate ways to make lookups faster.
  • Investigate what exact special-cased implementations we want to ship.

(@geeknoid, your help here would be appreciated. Best case numbers look very good, e.g. the example benchmarks you previously created, but others don't look nearly as good. I vetted some of the worst numbers against the original implementation, and they're similar.)

Right now, for each of dictionary/set, there are custom implementations for:

  • Int32 keys with the default comparer
  • Value type keys with the default comparer
  • String keys with default/ordinal/ordinal-ignore-case comparers. Here there are two: one that tries to bucket a small number of strings by length, and one that tries to avoid reading/hashing the full strings by selecting the smallest portion that's unique enough.
  • A general fallback implementation that's used for everything else.

We can explore other data structures, other optimizations, etc., all behind the same veneer. Some of the existing implementations might not be worthwhile, too. For some situations, we might even want an implementation that just wraps Dictionary<,> along with dedicated keys/values arrays.

But the first step is getting the APIs merged to enable others to experiment.

Some benchmarks
[InProcess][GenericTypeArguments(typeof(string))][GenericTypeArguments(typeof(int))][GenericTypeArguments(typeof(object))][GenericTypeArguments(typeof(Guid))]publicclassDictionaryTests<TKey>{privateTKey[]_keys;privateTKey[]_nonExistentKeys;privateDictionary<TKey,long>_dictionary;privateFrozenDictionary<TKey,long>_frozenDictionary;[Params(1,16,1024)]publicintCount{get;set;}[Params(false,true)]publicboolDefaultComparer{get;set;}privatestaticIEqualityComparer<TKey>GetComparer(booldefaultComparer)=>defaultComparer?EqualityComparer<TKey>.Default:NonDefaultEqualityComparer<TKey>.Instance;privateunsafestaticTRandGetRandomValue<TRand>(Randomrand){if(typeof(TRand)==typeof(string)){Span<char>span=stackallocchar[rand.Next(5,20)];for(inti=0;i<span.Length;i++){span[i]=(char)('a'+rand.Next(0,26));}return(TRand)(object)span.ToString();}if(typeof(TRand)==typeof(object)){return(TRand)newobject();}if(typeof(TRand)==typeof(Guid)){Int128value=newInt128((ulong)rand.NextInt64(),(ulong)rand.NextInt64());return(TRand)(object)(*(Guid*)(&value));}if(typeof(TRand)==typeof(int)){return(TRand)(object)rand.Next();}if(typeof(TRand)==typeof(long)){return(TRand)(object)rand.NextInt64();}thrownewException("Unknown type");}[GlobalSetup]publicvoidSetup(){varrand=newRandom(42);_dictionary=newDictionary<TKey,long>(GetComparer(DefaultComparer));for(intj=0;j<Count;j++){_dictionary[GetRandomValue<TKey>(rand)]=GetRandomValue<long>(rand);}_frozenDictionary=_dictionary.ToFrozenDictionary(GetComparer(DefaultComparer));_keys=_dictionary.Keys.ToArray();if(typeof(TKey)==typeof(string)){_keys=(TKey[])(object)Array.ConvertAll((string[])(object)_keys,string.Copy);}_nonExistentKeys=newTKey[Count];for(inti=0;i<Count;i++){TKeykey;while(_dictionary.ContainsKey(key=GetRandomValue<TKey>(rand)));_nonExistentKeys[i]=key;}}[Benchmark]publicDictionary<TKey,long>Dictionary_Construct(){return_keys.ToDictionary((TKeyk)=>k,(TKeyk)=>0L,GetComparer(DefaultComparer));}[Benchmark]publicFrozenDictionary<TKey,long>FrozenDictionary_Construct(){return_keys.ToFrozenDictionary((TKeyk)=>k,(TKeyk)=>0L,GetComparer(DefaultComparer));}[Benchmark]publicboolDictionary_TryGetValue_Found(){boolallFound=true;TKey[]keys=_keys;foreach(TKeykeyinkeys){longvalue;allFound&=_dictionary.TryGetValue(key,outvalue);}returnallFound;}[Benchmark]publicboolDictionary_TryGetValue_NotFound(){boolanyFound=false;TKey[]nonExistentKeys=_nonExistentKeys;foreach(TKeykeyinnonExistentKeys){longvalue;anyFound|=_dictionary.TryGetValue(key,outvalue);}returnanyFound;}[Benchmark]publicboolFrozenDictionary_TryGetValue_Found(){boolallFound=true;TKey[]keys=_keys;foreach(TKeykeyinkeys){longvalue;allFound&=_frozenDictionary.TryGetValue(key,outvalue);}returnallFound;}[Benchmark]publicboolFrozenDictionary_TryGetValue_NotFound(){boolanyFound=false;TKey[]nonExistentKeys=_nonExistentKeys;foreach(TKeykeyinnonExistentKeys){longvalue;anyFound|=_frozenDictionary.TryGetValue(key,outvalue);}returnanyFound;}[Benchmark]publicintDictionary_Enumerate(){intcount=0;foreach(KeyValuePair<TKey,long>itemin_dictionary){_=item;count++;}returncount;}[Benchmark]publicintDictionary_EnumerateKeys(){intcount=0;foreach(TKeykeyin_dictionary.Keys){_=key;count++;}returncount;}[Benchmark]publiclongDictionary_EnumerateValues(){intcount=0;foreach(longvaluein_dictionary.Values){_=value;count++;}returncount;}[Benchmark]publicintFrozenDictionary_Enumerate(){intcount=0;foreach(KeyValuePair<TKey,long>itemin_frozenDictionary){_=item;count++;}returncount;}[Benchmark]publicintFrozenDictionary_EnumerateKeys(){intcount=0;foreach(TKeykeyin_frozenDictionary.Keys){count++;}returncount;}[Benchmark]publicintFrozenDictionary_EnumerateValues(){intcount=0;foreach(longvaluein_frozenDictionary.Values){count++;}returncount;}}internalsealedpartialclassNonDefaultEqualityComparer<T>:EqualityComparer<T>{publicstaticNonDefaultEqualityComparer<T>Instance{get;}=newNonDefaultEqualityComparer<T>();publicoverrideboolEquals(Tx,Ty)=>Default.Equals(x,y);publicoverrideintGetHashCode(Tobj)=>Default.GetHashCode(obj);}
TypeMethodCountDefaultComparerMean
DictionaryTests<Guid>Dictionary_Construct16False308.917 ns
DictionaryTests<Guid>FrozenDictionary_Construct16False2,546.966 ns
DictionaryTests<Int32>Dictionary_Construct16False256.919 ns
DictionaryTests<Int32>FrozenDictionary_Construct16False2,735.513 ns
DictionaryTests<Object>Dictionary_Construct16False459.599 ns
DictionaryTests<Object>FrozenDictionary_Construct16False2,990.372 ns
DictionaryTests<String>Dictionary_Construct16False574.025 ns
DictionaryTests<String>FrozenDictionary_Construct16False3,085.790 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found16False195.613 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found16False180.628 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found16False127.828 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found16False120.595 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found16False371.343 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found16False384.100 ns
DictionaryTests<String>Dictionary_TryGetValue_Found16False559.943 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found16False531.615 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound16False149.167 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound16False120.109 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound16False109.596 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound16False91.319 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound16False247.097 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound16False231.592 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound16False356.568 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound16False334.678 ns
DictionaryTests<Guid>Dictionary_Enumerate16False61.120 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate16False27.948 ns
DictionaryTests<Int32>Dictionary_Enumerate16False50.372 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate16False27.162 ns
DictionaryTests<Object>Dictionary_Enumerate16False89.827 ns
DictionaryTests<Object>FrozenDictionary_Enumerate16False74.392 ns
DictionaryTests<String>Dictionary_Enumerate16False89.446 ns
DictionaryTests<String>FrozenDictionary_Enumerate16False74.626 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys16False49.057 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys16False4.847 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys16False37.000 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys16False5.108 ns
DictionaryTests<Object>Dictionary_EnumerateKeys16False74.497 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys16False18.488 ns
DictionaryTests<String>Dictionary_EnumerateKeys16False74.906 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys16False18.571 ns
DictionaryTests<Guid>Dictionary_EnumerateValues16False38.145 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues16False5.054 ns
DictionaryTests<Int32>Dictionary_EnumerateValues16False36.834 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues16False5.103 ns
DictionaryTests<Object>Dictionary_EnumerateValues16False54.844 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues16False5.045 ns
DictionaryTests<String>Dictionary_EnumerateValues16False55.299 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues16False5.048 ns
DictionaryTests<Guid>Dictionary_Construct16True276.233 ns
DictionaryTests<Guid>FrozenDictionary_Construct16True2,480.023 ns
DictionaryTests<Int32>Dictionary_Construct16True247.043 ns
DictionaryTests<Int32>FrozenDictionary_Construct16True2,679.061 ns
DictionaryTests<Object>Dictionary_Construct16True401.734 ns
DictionaryTests<Object>FrozenDictionary_Construct16True2,782.085 ns
DictionaryTests<String>Dictionary_Construct16True360.930 ns
DictionaryTests<String>FrozenDictionary_Construct16True1,385.360 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found16True116.494 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found16True124.232 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found16True87.759 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found16True50.979 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found16True207.784 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found16True209.461 ns
DictionaryTests<String>Dictionary_TryGetValue_Found16True268.939 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found16True142.170 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound16True99.116 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound16True81.180 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound16True72.956 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound16True53.239 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound16True163.937 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound16True144.715 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound16True178.219 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound16True109.456 ns
DictionaryTests<Guid>Dictionary_Enumerate16True58.001 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate16True30.898 ns
DictionaryTests<Int32>Dictionary_Enumerate16True49.878 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate16True27.879 ns
DictionaryTests<Object>Dictionary_Enumerate16True89.847 ns
DictionaryTests<Object>FrozenDictionary_Enumerate16True74.568 ns
DictionaryTests<String>Dictionary_Enumerate16True89.347 ns
DictionaryTests<String>FrozenDictionary_Enumerate16True73.377 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys16True48.112 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys16True5.259 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys16True36.038 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys16True5.016 ns
DictionaryTests<Object>Dictionary_EnumerateKeys16True74.331 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys16True19.002 ns
DictionaryTests<String>Dictionary_EnumerateKeys16True73.520 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys16True18.961 ns
DictionaryTests<Guid>Dictionary_EnumerateValues16True39.389 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues16True5.432 ns
DictionaryTests<Int32>Dictionary_EnumerateValues16True37.072 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues16True4.999 ns
DictionaryTests<Object>Dictionary_EnumerateValues16True54.973 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues16True5.033 ns
DictionaryTests<String>Dictionary_EnumerateValues16True54.496 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues16True5.029 ns
DictionaryTests<Guid>Dictionary_Construct1024False20,251.116 ns
DictionaryTests<Guid>FrozenDictionary_Construct1024False165,973.254 ns
DictionaryTests<Int32>Dictionary_Construct1024False16,858.288 ns
DictionaryTests<Int32>FrozenDictionary_Construct1024False149,391.455 ns
DictionaryTests<Object>Dictionary_Construct1024False26,154.444 ns
DictionaryTests<Object>FrozenDictionary_Construct1024False174,545.846 ns
DictionaryTests<String>Dictionary_Construct1024False36,381.752 ns
DictionaryTests<String>FrozenDictionary_Construct1024False200,478.010 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found1024False16,232.339 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found1024False14,450.573 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found1024False10,903.514 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found1024False9,294.073 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found1024False24,834.107 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found1024False27,807.133 ns
DictionaryTests<String>Dictionary_TryGetValue_Found1024False43,093.660 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found1024False39,181.201 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound1024False13,773.527 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound1024False8,281.616 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound1024False10,252.792 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound1024False6,199.940 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound1024False20,427.130 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound1024False15,131.280 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound1024False26,598.257 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound1024False24,474.515 ns
DictionaryTests<Guid>Dictionary_Enumerate1024False3,440.145 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate1024False1,689.686 ns
DictionaryTests<Int32>Dictionary_Enumerate1024False2,520.821 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate1024False1,498.062 ns
DictionaryTests<Object>Dictionary_Enumerate1024False4,408.578 ns
DictionaryTests<Object>FrozenDictionary_Enumerate1024False3,689.598 ns
DictionaryTests<String>Dictionary_Enumerate1024False4,398.440 ns
DictionaryTests<String>FrozenDictionary_Enumerate1024False3,636.864 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys1024False2,359.947 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys1024False261.473 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys1024False2,033.189 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys1024False253.029 ns
DictionaryTests<Object>Dictionary_EnumerateKeys1024False3,742.122 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys1024False1,226.121 ns
DictionaryTests<String>Dictionary_EnumerateKeys1024False3,693.788 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys1024False1,222.912 ns
DictionaryTests<Guid>Dictionary_EnumerateValues1024False2,144.243 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues1024False260.725 ns
DictionaryTests<Int32>Dictionary_EnumerateValues1024False2,039.401 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues1024False255.137 ns
DictionaryTests<Object>Dictionary_EnumerateValues1024False2,900.210 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues1024False254.610 ns
DictionaryTests<String>Dictionary_EnumerateValues1024False2,924.998 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues1024False252.582 ns
DictionaryTests<Guid>Dictionary_Construct1024True17,653.020 ns
DictionaryTests<Guid>FrozenDictionary_Construct1024True165,900.270 ns
DictionaryTests<Int32>Dictionary_Construct1024True15,324.775 ns
DictionaryTests<Int32>FrozenDictionary_Construct1024True145,562.184 ns
DictionaryTests<Object>Dictionary_Construct1024True22,357.810 ns
DictionaryTests<Object>FrozenDictionary_Construct1024True164,976.669 ns
DictionaryTests<String>Dictionary_Construct1024True24,584.158 ns
DictionaryTests<String>FrozenDictionary_Construct1024True713,506.562 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found1024True10,972.369 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found1024True10,677.337 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found1024True6,061.961 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found1024True4,568.878 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found1024True15,521.277 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found1024True15,039.072 ns
DictionaryTests<String>Dictionary_TryGetValue_Found1024True21,252.949 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found1024True19,273.593 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound1024True10,827.871 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound1024True6,294.780 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound1024True8,168.398 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound1024True4,152.874 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound1024True14,748.903 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound1024True9,459.935 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound1024True20,727.839 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound1024True11,941.794 ns
DictionaryTests<Guid>Dictionary_Enumerate1024True3,355.945 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate1024True1,676.260 ns
DictionaryTests<Int32>Dictionary_Enumerate1024True2,501.281 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate1024True1,488.353 ns
DictionaryTests<Object>Dictionary_Enumerate1024True4,433.532 ns
DictionaryTests<Object>FrozenDictionary_Enumerate1024True3,662.125 ns
DictionaryTests<String>Dictionary_Enumerate1024True4,485.860 ns
DictionaryTests<String>FrozenDictionary_Enumerate1024True3,652.590 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys1024True2,351.717 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys1024True262.712 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys1024True2,019.313 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys1024True252.664 ns
DictionaryTests<Object>Dictionary_EnumerateKeys1024True3,673.043 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys1024True1,224.610 ns
DictionaryTests<String>Dictionary_EnumerateKeys1024True3,748.785 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys1024True1,228.282 ns
DictionaryTests<Guid>Dictionary_EnumerateValues1024True2,080.066 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues1024True255.457 ns
DictionaryTests<Int32>Dictionary_EnumerateValues1024True2,018.673 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues1024True255.488 ns
DictionaryTests<Object>Dictionary_EnumerateValues1024True2,872.265 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues1024True255.872 ns
DictionaryTests<String>Dictionary_EnumerateValues1024True2,895.610 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues1024True254.709 ns

@ghost

ghost commented Nov 2, 2022

Copy link
Copy Markdown

Note regarding the new-api-needs-documentation label:

This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, to please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change.

@ghost

ghost commented Nov 2, 2022

Copy link
Copy Markdown

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

Issue Details

The first commit imports @geeknoid's source, updated to compile in System.Collections.Immutable.dll.

The second commit overhauls the APIs and adds tests.

This includes a bunch of updates to our shared collections tests, which we've apparently not used in earnest with read-only collections, as a bunch of mutating tests were trying to run even when IsReadOnly was set to false.

There's still more work required here. Subsequent to this PR, we should:

  • Investigate ways to make construction faster.
  • Investigate ways to make lookups faster.
  • Investigate what exact special-cased implementations we want to ship.

(@geeknoid, your help here would be appreciated. Best case numbers look very good, e.g. the example benchmarks you previously created, but others don't look nearly as good. I vetted some of the worst numbers against the original implementation, and they're similar.)

Right now, for each of dictionary/set, there are custom implementations for:

  • Int32 keys with the default comparer
  • Value type keys with the default comparer
  • String keys with default/ordinal/ordinal-ignore-case comparers. Here there are two: one that tries to bucket a small number of strings by length, and one that tries to avoid reading/hashing the full strings by selecting the smallest portion that's unique enough.
  • A general fallback implementation that's used for everything else.

We can explore other data structures, other optimizations, etc., all behind the same veneer. Some of the existing implementations might not be worthwhile, too. For some situations, we might even want an implementation that just wraps Dictionary<,> along with dedicated keys/values arrays.

But the first step is getting the APIs merged to enable others to experiment.

Some benchmarks
[InProcess][GenericTypeArguments(typeof(string))][GenericTypeArguments(typeof(int))][GenericTypeArguments(typeof(object))][GenericTypeArguments(typeof(Guid))]publicclassDictionaryTests<TKey>{privateTKey[]_keys;privateTKey[]_nonExistentKeys;privateDictionary<TKey,long>_dictionary;privateFrozenDictionary<TKey,long>_frozenDictionary;[Params(1,16,1024)]publicintCount{get;set;}[Params(false,true)]publicboolDefaultComparer{get;set;}privatestaticIEqualityComparer<TKey>GetComparer(booldefaultComparer)=>defaultComparer?EqualityComparer<TKey>.Default:NonDefaultEqualityComparer<TKey>.Instance;privateunsafestaticTRandGetRandomValue<TRand>(Randomrand){if(typeof(TRand)==typeof(string)){Span<char>span=stackallocchar[rand.Next(5,20)];for(inti=0;i<span.Length;i++){span[i]=(char)('a'+rand.Next(0,26));}return(TRand)(object)span.ToString();}if(typeof(TRand)==typeof(object)){return(TRand)newobject();}if(typeof(TRand)==typeof(Guid)){Int128value=newInt128((ulong)rand.NextInt64(),(ulong)rand.NextInt64());return(TRand)(object)(*(Guid*)(&value));}if(typeof(TRand)==typeof(int)){return(TRand)(object)rand.Next();}if(typeof(TRand)==typeof(long)){return(TRand)(object)rand.NextInt64();}thrownewException("Unknown type");}[GlobalSetup]publicvoidSetup(){varrand=newRandom(42);_dictionary=newDictionary<TKey,long>(GetComparer(DefaultComparer));for(intj=0;j<Count;j++){_dictionary[GetRandomValue<TKey>(rand)]=GetRandomValue<long>(rand);}_frozenDictionary=_dictionary.ToFrozenDictionary(GetComparer(DefaultComparer));_keys=_dictionary.Keys.ToArray();if(typeof(TKey)==typeof(string)){_keys=(TKey[])(object)Array.ConvertAll((string[])(object)_keys,string.Copy);}_nonExistentKeys=newTKey[Count];for(inti=0;i<Count;i++){TKeykey;while(_dictionary.ContainsKey(key=GetRandomValue<TKey>(rand)));_nonExistentKeys[i]=key;}}[Benchmark]publicDictionary<TKey,long>Dictionary_Construct(){return_keys.ToDictionary((TKeyk)=>k,(TKeyk)=>0L,GetComparer(DefaultComparer));}[Benchmark]publicFrozenDictionary<TKey,long>FrozenDictionary_Construct(){return_keys.ToFrozenDictionary((TKeyk)=>k,(TKeyk)=>0L,GetComparer(DefaultComparer));}[Benchmark]publicboolDictionary_TryGetValue_Found(){boolallFound=true;TKey[]keys=_keys;foreach(TKeykeyinkeys){longvalue;allFound&=_dictionary.TryGetValue(key,outvalue);}returnallFound;}[Benchmark]publicboolDictionary_TryGetValue_NotFound(){boolanyFound=false;TKey[]nonExistentKeys=_nonExistentKeys;foreach(TKeykeyinnonExistentKeys){longvalue;anyFound|=_dictionary.TryGetValue(key,outvalue);}returnanyFound;}[Benchmark]publicboolFrozenDictionary_TryGetValue_Found(){boolallFound=true;TKey[]keys=_keys;foreach(TKeykeyinkeys){longvalue;allFound&=_frozenDictionary.TryGetValue(key,outvalue);}returnallFound;}[Benchmark]publicboolFrozenDictionary_TryGetValue_NotFound(){boolanyFound=false;TKey[]nonExistentKeys=_nonExistentKeys;foreach(TKeykeyinnonExistentKeys){longvalue;anyFound|=_frozenDictionary.TryGetValue(key,outvalue);}returnanyFound;}[Benchmark]publicintDictionary_Enumerate(){intcount=0;foreach(KeyValuePair<TKey,long>itemin_dictionary){_=item;count++;}returncount;}[Benchmark]publicintDictionary_EnumerateKeys(){intcount=0;foreach(TKeykeyin_dictionary.Keys){_=key;count++;}returncount;}[Benchmark]publiclongDictionary_EnumerateValues(){intcount=0;foreach(longvaluein_dictionary.Values){_=value;count++;}returncount;}[Benchmark]publicintFrozenDictionary_Enumerate(){intcount=0;foreach(KeyValuePair<TKey,long>itemin_frozenDictionary){_=item;count++;}returncount;}[Benchmark]publicintFrozenDictionary_EnumerateKeys(){intcount=0;foreach(TKeykeyin_frozenDictionary.Keys){count++;}returncount;}[Benchmark]publicintFrozenDictionary_EnumerateValues(){intcount=0;foreach(longvaluein_frozenDictionary.Values){count++;}returncount;}}internalsealedpartialclassNonDefaultEqualityComparer<T>:EqualityComparer<T>{publicstaticNonDefaultEqualityComparer<T>Instance{get;}=newNonDefaultEqualityComparer<T>();publicoverrideboolEquals(Tx,Ty)=>Default.Equals(x,y);publicoverrideintGetHashCode(Tobj)=>Default.GetHashCode(obj);}
TypeMethodCountDefaultComparerMean
DictionaryTests<Guid>Dictionary_Construct16False308.917 ns
DictionaryTests<Guid>FrozenDictionary_Construct16False2,546.966 ns
DictionaryTests<Int32>Dictionary_Construct16False256.919 ns
DictionaryTests<Int32>FrozenDictionary_Construct16False2,735.513 ns
DictionaryTests<Object>Dictionary_Construct16False459.599 ns
DictionaryTests<Object>FrozenDictionary_Construct16False2,990.372 ns
DictionaryTests<String>Dictionary_Construct16False574.025 ns
DictionaryTests<String>FrozenDictionary_Construct16False3,085.790 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found16False195.613 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found16False180.628 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found16False127.828 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found16False120.595 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found16False371.343 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found16False384.100 ns
DictionaryTests<String>Dictionary_TryGetValue_Found16False559.943 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found16False531.615 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound16False149.167 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound16False120.109 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound16False109.596 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound16False91.319 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound16False247.097 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound16False231.592 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound16False356.568 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound16False334.678 ns
DictionaryTests<Guid>Dictionary_Enumerate16False61.120 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate16False27.948 ns
DictionaryTests<Int32>Dictionary_Enumerate16False50.372 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate16False27.162 ns
DictionaryTests<Object>Dictionary_Enumerate16False89.827 ns
DictionaryTests<Object>FrozenDictionary_Enumerate16False74.392 ns
DictionaryTests<String>Dictionary_Enumerate16False89.446 ns
DictionaryTests<String>FrozenDictionary_Enumerate16False74.626 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys16False49.057 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys16False4.847 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys16False37.000 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys16False5.108 ns
DictionaryTests<Object>Dictionary_EnumerateKeys16False74.497 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys16False18.488 ns
DictionaryTests<String>Dictionary_EnumerateKeys16False74.906 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys16False18.571 ns
DictionaryTests<Guid>Dictionary_EnumerateValues16False38.145 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues16False5.054 ns
DictionaryTests<Int32>Dictionary_EnumerateValues16False36.834 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues16False5.103 ns
DictionaryTests<Object>Dictionary_EnumerateValues16False54.844 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues16False5.045 ns
DictionaryTests<String>Dictionary_EnumerateValues16False55.299 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues16False5.048 ns
DictionaryTests<Guid>Dictionary_Construct16True276.233 ns
DictionaryTests<Guid>FrozenDictionary_Construct16True2,480.023 ns
DictionaryTests<Int32>Dictionary_Construct16True247.043 ns
DictionaryTests<Int32>FrozenDictionary_Construct16True2,679.061 ns
DictionaryTests<Object>Dictionary_Construct16True401.734 ns
DictionaryTests<Object>FrozenDictionary_Construct16True2,782.085 ns
DictionaryTests<String>Dictionary_Construct16True360.930 ns
DictionaryTests<String>FrozenDictionary_Construct16True1,385.360 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found16True116.494 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found16True124.232 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found16True87.759 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found16True50.979 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found16True207.784 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found16True209.461 ns
DictionaryTests<String>Dictionary_TryGetValue_Found16True268.939 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found16True142.170 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound16True99.116 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound16True81.180 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound16True72.956 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound16True53.239 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound16True163.937 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound16True144.715 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound16True178.219 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound16True109.456 ns
DictionaryTests<Guid>Dictionary_Enumerate16True58.001 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate16True30.898 ns
DictionaryTests<Int32>Dictionary_Enumerate16True49.878 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate16True27.879 ns
DictionaryTests<Object>Dictionary_Enumerate16True89.847 ns
DictionaryTests<Object>FrozenDictionary_Enumerate16True74.568 ns
DictionaryTests<String>Dictionary_Enumerate16True89.347 ns
DictionaryTests<String>FrozenDictionary_Enumerate16True73.377 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys16True48.112 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys16True5.259 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys16True36.038 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys16True5.016 ns
DictionaryTests<Object>Dictionary_EnumerateKeys16True74.331 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys16True19.002 ns
DictionaryTests<String>Dictionary_EnumerateKeys16True73.520 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys16True18.961 ns
DictionaryTests<Guid>Dictionary_EnumerateValues16True39.389 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues16True5.432 ns
DictionaryTests<Int32>Dictionary_EnumerateValues16True37.072 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues16True4.999 ns
DictionaryTests<Object>Dictionary_EnumerateValues16True54.973 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues16True5.033 ns
DictionaryTests<String>Dictionary_EnumerateValues16True54.496 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues16True5.029 ns
DictionaryTests<Guid>Dictionary_Construct1024False20,251.116 ns
DictionaryTests<Guid>FrozenDictionary_Construct1024False165,973.254 ns
DictionaryTests<Int32>Dictionary_Construct1024False16,858.288 ns
DictionaryTests<Int32>FrozenDictionary_Construct1024False149,391.455 ns
DictionaryTests<Object>Dictionary_Construct1024False26,154.444 ns
DictionaryTests<Object>FrozenDictionary_Construct1024False174,545.846 ns
DictionaryTests<String>Dictionary_Construct1024False36,381.752 ns
DictionaryTests<String>FrozenDictionary_Construct1024False200,478.010 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found1024False16,232.339 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found1024False14,450.573 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found1024False10,903.514 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found1024False9,294.073 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found1024False24,834.107 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found1024False27,807.133 ns
DictionaryTests<String>Dictionary_TryGetValue_Found1024False43,093.660 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found1024False39,181.201 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound1024False13,773.527 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound1024False8,281.616 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound1024False10,252.792 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound1024False6,199.940 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound1024False20,427.130 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound1024False15,131.280 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound1024False26,598.257 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound1024False24,474.515 ns
DictionaryTests<Guid>Dictionary_Enumerate1024False3,440.145 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate1024False1,689.686 ns
DictionaryTests<Int32>Dictionary_Enumerate1024False2,520.821 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate1024False1,498.062 ns
DictionaryTests<Object>Dictionary_Enumerate1024False4,408.578 ns
DictionaryTests<Object>FrozenDictionary_Enumerate1024False3,689.598 ns
DictionaryTests<String>Dictionary_Enumerate1024False4,398.440 ns
DictionaryTests<String>FrozenDictionary_Enumerate1024False3,636.864 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys1024False2,359.947 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys1024False261.473 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys1024False2,033.189 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys1024False253.029 ns
DictionaryTests<Object>Dictionary_EnumerateKeys1024False3,742.122 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys1024False1,226.121 ns
DictionaryTests<String>Dictionary_EnumerateKeys1024False3,693.788 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys1024False1,222.912 ns
DictionaryTests<Guid>Dictionary_EnumerateValues1024False2,144.243 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues1024False260.725 ns
DictionaryTests<Int32>Dictionary_EnumerateValues1024False2,039.401 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues1024False255.137 ns
DictionaryTests<Object>Dictionary_EnumerateValues1024False2,900.210 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues1024False254.610 ns
DictionaryTests<String>Dictionary_EnumerateValues1024False2,924.998 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues1024False252.582 ns
DictionaryTests<Guid>Dictionary_Construct1024True17,653.020 ns
DictionaryTests<Guid>FrozenDictionary_Construct1024True165,900.270 ns
DictionaryTests<Int32>Dictionary_Construct1024True15,324.775 ns
DictionaryTests<Int32>FrozenDictionary_Construct1024True145,562.184 ns
DictionaryTests<Object>Dictionary_Construct1024True22,357.810 ns
DictionaryTests<Object>FrozenDictionary_Construct1024True164,976.669 ns
DictionaryTests<String>Dictionary_Construct1024True24,584.158 ns
DictionaryTests<String>FrozenDictionary_Construct1024True713,506.562 ns
DictionaryTests<Guid>Dictionary_TryGetValue_Found1024True10,972.369 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_Found1024True10,677.337 ns
DictionaryTests<Int32>Dictionary_TryGetValue_Found1024True6,061.961 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_Found1024True4,568.878 ns
DictionaryTests<Object>Dictionary_TryGetValue_Found1024True15,521.277 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_Found1024True15,039.072 ns
DictionaryTests<String>Dictionary_TryGetValue_Found1024True21,252.949 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_Found1024True19,273.593 ns
DictionaryTests<Guid>Dictionary_TryGetValue_NotFound1024True10,827.871 ns
DictionaryTests<Guid>FrozenDictionary_TryGetValue_NotFound1024True6,294.780 ns
DictionaryTests<Int32>Dictionary_TryGetValue_NotFound1024True8,168.398 ns
DictionaryTests<Int32>FrozenDictionary_TryGetValue_NotFound1024True4,152.874 ns
DictionaryTests<Object>Dictionary_TryGetValue_NotFound1024True14,748.903 ns
DictionaryTests<Object>FrozenDictionary_TryGetValue_NotFound1024True9,459.935 ns
DictionaryTests<String>Dictionary_TryGetValue_NotFound1024True20,727.839 ns
DictionaryTests<String>FrozenDictionary_TryGetValue_NotFound1024True11,941.794 ns
DictionaryTests<Guid>Dictionary_Enumerate1024True3,355.945 ns
DictionaryTests<Guid>FrozenDictionary_Enumerate1024True1,676.260 ns
DictionaryTests<Int32>Dictionary_Enumerate1024True2,501.281 ns
DictionaryTests<Int32>FrozenDictionary_Enumerate1024True1,488.353 ns
DictionaryTests<Object>Dictionary_Enumerate1024True4,433.532 ns
DictionaryTests<Object>FrozenDictionary_Enumerate1024True3,662.125 ns
DictionaryTests<String>Dictionary_Enumerate1024True4,485.860 ns
DictionaryTests<String>FrozenDictionary_Enumerate1024True3,652.590 ns
DictionaryTests<Guid>Dictionary_EnumerateKeys1024True2,351.717 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateKeys1024True262.712 ns
DictionaryTests<Int32>Dictionary_EnumerateKeys1024True2,019.313 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateKeys1024True252.664 ns
DictionaryTests<Object>Dictionary_EnumerateKeys1024True3,673.043 ns
DictionaryTests<Object>FrozenDictionary_EnumerateKeys1024True1,224.610 ns
DictionaryTests<String>Dictionary_EnumerateKeys1024True3,748.785 ns
DictionaryTests<String>FrozenDictionary_EnumerateKeys1024True1,228.282 ns
DictionaryTests<Guid>Dictionary_EnumerateValues1024True2,080.066 ns
DictionaryTests<Guid>FrozenDictionary_EnumerateValues1024True255.457 ns
DictionaryTests<Int32>Dictionary_EnumerateValues1024True2,018.673 ns
DictionaryTests<Int32>FrozenDictionary_EnumerateValues1024True255.488 ns
DictionaryTests<Object>Dictionary_EnumerateValues1024True2,872.265 ns
DictionaryTests<Object>FrozenDictionary_EnumerateValues1024True255.872 ns
DictionaryTests<String>Dictionary_EnumerateValues1024True2,895.610 ns
DictionaryTests<String>FrozenDictionary_EnumerateValues1024True254.709 ns
Author:stephentoub
Assignees:-
Labels:

area-System.Collections

Milestone:8.0.0

@ghostghost assigned stephentoubNov 2, 2022
@geeknoid

Copy link
Copy Markdown
Member

The read benchmarks look pretty good, but the construction phase is definitely on the slow side. I never really measure the construction time since for my scenarios I didn't care so much. I do think adding some notion of "intensity" in the constructor to let the caller decide how much time to spend optimizing would be desirable.

It would be interesting in particular to see what the read perf is if the implementation doesn't perform any optimization at all. It would likely still be faster than a normal Dictionary/HashSet when using the default comparers, but not by much. But if that case is still a bit faster and construction time is on par with normal Dictionary/HashSet, then that says that any scenario that needs a readonly dictionary or set should be using the frozen ones by default since it would be faster.

Without the ability to tune how much time is spent constructing, then you have to be careful where you use the frozen collections and make sure the cost/benefit will be worth it.

@geeknoid

geeknoid commented Nov 2, 2022

Copy link
Copy Markdown
Member

Some thoughts for the future:

  • Dictionaries and sets with low cardinality are common. We should look at adding specialized implementation for 1 and 2 entry collections. This saves memory and speeds things up.

  • I had on my todo list to create specialized two character string comparers to optimize that case. For small collections, often the single char comparer is enough and works well. But when the collection grows, you run out of single characters to compare, so the code falls back to two.

  • We discussed in the review that the majority use case for this kind of collection is where the data comes dynamically into the process. This says that doing the optimization work for the collections in a source generator is not likely to be used frequently. However, what I think might be useful is the ability to serialize/deserialize the internal state for the frozen collections in a portable way. This would let me distribute this serialized state as config, and thus forego the construction overhead. So instead of putting a bunch of key/value pairs in my config and having to optimize those in each of my service's processes, I could optimize the key/value pairs offline and push the resulting state to the processes.

stephentouband others added 2 commits November 2, 2022 17:45
…y compile
Co-authored-by: Martin Taillefer <mataille@microsoft.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>

@geeknoidgeeknoid left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This looks great.

@krwq

krwq commented Nov 4, 2022

Copy link
Copy Markdown
Member

I'd be really interested to see how integration of this feature would look like with System.Text.Json (both JsonSerializerOptions.Converters and JsonTypeInfo.Properties are freezing values at one point)

krwq
krwq approved these changes Nov 4, 2022

@krwqkrwq left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I've skimmed all of the files and overall looks good. I'm really impressed in the amount of heuristics done here to make this super optimized. Thank you!

@stephentoub
stephentoub merged commit 6c5a440 into dotnet:mainNov 4, 2022
@stephentoub
stephentoub deleted the frozencollections branch November 4, 2022 19:49
@ghostghost locked as resolved and limited conversation to collaborators Dec 5, 2022
@jeffhandleyjeffhandley added the blog-candidate Completed PRs that are candidate topics for blog post coverage label Mar 14, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Collectionsblog-candidateCompleted PRs that are candidate topics for blog post coveragenew-api-needs-documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[API Proposal]: Provide optimized read-only collections

4 participants

@stephentoub@geeknoid@krwq@jeffhandley