-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add FrozenDictionary/Set #77799
Add FrozenDictionary/Set #77799
Conversation
Note regarding the 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. |
Tagging subscribers to this area: @dotnet/area-system-collections Issue DetailsThe 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:
(@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:
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 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))]
public class DictionaryTests<TKey>
{
private TKey[] _keys;
private TKey[] _nonExistentKeys;
private Dictionary<TKey, long> _dictionary;
private FrozenDictionary<TKey, long> _frozenDictionary;
[Params(1, 16, 1024)]
public int Count { get; set; }
[Params(false, true)]
public bool DefaultComparer { get; set; }
private static IEqualityComparer<TKey> GetComparer(bool defaultComparer) =>
defaultComparer ? EqualityComparer<TKey>.Default : NonDefaultEqualityComparer<TKey>.Instance;
private unsafe static TRand GetRandomValue<TRand>(Random rand)
{
if (typeof(TRand) == typeof(string))
{
Span<char> span = stackalloc char[rand.Next(5, 20)];
for (int i = 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)new object();
}
if (typeof(TRand) == typeof(Guid))
{
Int128 value = new Int128((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();
}
throw new Exception("Unknown type");
}
[GlobalSetup]
public void Setup()
{
var rand = new Random(42);
_dictionary = new Dictionary<TKey, long>(GetComparer(DefaultComparer));
for (int j = 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 = new TKey[Count];
for (int i = 0; i < Count; i++)
{
TKey key;
while (_dictionary.ContainsKey(key = GetRandomValue<TKey>(rand))) ;
_nonExistentKeys[i] = key;
}
}
[Benchmark]
public Dictionary<TKey, long> Dictionary_Construct()
{
return _keys.ToDictionary((TKey k) => k, (TKey k) => 0L, GetComparer(DefaultComparer));
}
[Benchmark]
public FrozenDictionary<TKey, long> FrozenDictionary_Construct()
{
return _keys.ToFrozenDictionary((TKey k) => k, (TKey k) => 0L, GetComparer(DefaultComparer));
}
[Benchmark]
public bool Dictionary_TryGetValue_Found()
{
bool allFound = true;
TKey[] keys = _keys;
foreach (TKey key in keys)
{
long value;
allFound &= _dictionary.TryGetValue(key, out value);
}
return allFound;
}
[Benchmark]
public bool Dictionary_TryGetValue_NotFound()
{
bool anyFound = false;
TKey[] nonExistentKeys = _nonExistentKeys;
foreach (TKey key in nonExistentKeys)
{
long value;
anyFound |= _dictionary.TryGetValue(key, out value);
}
return anyFound;
}
[Benchmark]
public bool FrozenDictionary_TryGetValue_Found()
{
bool allFound = true;
TKey[] keys = _keys;
foreach (TKey key in keys)
{
long value;
allFound &= _frozenDictionary.TryGetValue(key, out value);
}
return allFound;
}
[Benchmark]
public bool FrozenDictionary_TryGetValue_NotFound()
{
bool anyFound = false;
TKey[] nonExistentKeys = _nonExistentKeys;
foreach (TKey key in nonExistentKeys)
{
long value;
anyFound |= _frozenDictionary.TryGetValue(key, out value);
}
return anyFound;
}
[Benchmark]
public int Dictionary_Enumerate()
{
int count = 0;
foreach (KeyValuePair<TKey, long> item in _dictionary)
{
_ = item;
count++;
}
return count;
}
[Benchmark]
public int Dictionary_EnumerateKeys()
{
int count = 0;
foreach (TKey key in _dictionary.Keys)
{
_ = key;
count++;
}
return count;
}
[Benchmark]
public long Dictionary_EnumerateValues()
{
int count = 0;
foreach (long value in _dictionary.Values)
{
_ = value;
count++;
}
return count;
}
[Benchmark]
public int FrozenDictionary_Enumerate()
{
int count = 0;
foreach (KeyValuePair<TKey, long> item in _frozenDictionary)
{
_ = item;
count++;
}
return count;
}
[Benchmark]
public int FrozenDictionary_EnumerateKeys()
{
int count = 0;
foreach (TKey key in _frozenDictionary.Keys)
{
count++;
}
return count;
}
[Benchmark]
public int FrozenDictionary_EnumerateValues()
{
int count = 0;
foreach (long value in _frozenDictionary.Values)
{
count++;
}
return count;
}
}
internal sealed partial class NonDefaultEqualityComparer<T> : EqualityComparer<T>
{
public static NonDefaultEqualityComparer<T> Instance { get; } = new NonDefaultEqualityComparer<T>();
public override bool Equals(T x, T y) => Default.Equals(x, y);
public override int GetHashCode(T obj) => Default.GetHashCode(obj);
}
|
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. |
Some thoughts for the future:
|
…y compile Co-authored-by: Martin Taillefer <mataille@microsoft.com> Co-authored-by: Stephen Toub <stoub@microsoft.com>
d0dd3b2
to
7a025a6
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks great.
...ibraries/System.Collections.Immutable/src/System/Collections/Frozen/EmptyFrozenDictionary.cs
Outdated
Show resolved
Hide resolved
...braries/System.Collections.Immutable/src/System/Collections/Frozen/OrdinalStringFrozenSet.cs
Show resolved
Hide resolved
c6deda4
to
518574c
Compare
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) |
src/libraries/Common/tests/System/Collections/IDictionary.NonGeneric.Tests.cs
Show resolved
Hide resolved
src/libraries/System.Collections.Immutable/src/System.Collections.Immutable.csproj
Show resolved
Hide resolved
There was a problem hiding this comment.
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!
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:
(@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:
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
DictionaryTests<Guid>
DictionaryTests<Guid>
DictionaryTests<Int32>
DictionaryTests<Int32>
DictionaryTests<Object>
DictionaryTests<Object>
DictionaryTests<String>
DictionaryTests<String>
DictionaryTests<Guid>
DictionaryTests<Guid>
DictionaryTests<Int32>
DictionaryTests<Int32>
DictionaryTests<Object>
DictionaryTests<Object>
DictionaryTests<String>
DictionaryTests<String>
DictionaryTests<Guid>
DictionaryTests<Guid>
DictionaryTests<Int32>
DictionaryTests<Int32>
DictionaryTests<Object>
DictionaryTests<Object>
DictionaryTests<String>
DictionaryTests<String>
DictionaryTests<Guid>
DictionaryTests<Guid>
DictionaryTests<Int32>
DictionaryTests<Int32>
DictionaryTests<Object>
DictionaryTests<Object>
DictionaryTests<String>
DictionaryTests<String>
DictionaryTests<Guid>
DictionaryTests<Guid>
DictionaryTests<Int32>
DictionaryTests<Int32>
DictionaryTests<Object>
DictionaryTests<Object>
DictionaryTests<String>
DictionaryTests<String>
DictionaryTests<Guid>
DictionaryTests<Guid>
DictionaryTests<Int32>
DictionaryTests<Int32>
DictionaryTests<Object>
DictionaryTests<Object>
DictionaryTests<String>
DictionaryTests<String>
DictionaryTests<Guid>
DictionaryTests<Guid>
DictionaryTests<Int32>
DictionaryTests<Int32>
DictionaryTests<Object>
DictionaryTests<Object>
DictionaryTests<String>
DictionaryTests<String>
DictionaryTests<Guid>
DictionaryTests<Guid>
DictionaryTests<Int32>
DictionaryTests<Int32>
DictionaryTests<Object>
DictionaryTests<Object>
DictionaryTests<String>
DictionaryTests<String>
DictionaryTests<Guid>
DictionaryTests<Guid>
DictionaryTests<Int32>
DictionaryTests<Int32>
DictionaryTests<Object>
DictionaryTests<Object>
DictionaryTests<String>
DictionaryTests<String>
DictionaryTests<Guid>
DictionaryTests<Guid>
DictionaryTests<Int32>
DictionaryTests<Int32>
DictionaryTests<Object>
DictionaryTests<Object>
DictionaryTests<String>
DictionaryTests<String>
DictionaryTests<Guid>
DictionaryTests<Guid>
DictionaryTests<Int32>
DictionaryTests<Int32>
DictionaryTests<Object>
DictionaryTests<Object>
DictionaryTests<String>
DictionaryTests<String>
DictionaryTests<Guid>
DictionaryTests<Guid>
DictionaryTests<Int32>
DictionaryTests<Int32>
DictionaryTests<Object>
DictionaryTests<Object>
DictionaryTests<String>
DictionaryTests<String>
DictionaryTests<Guid>
DictionaryTests<Guid>
DictionaryTests<Int32>
DictionaryTests<Int32>
DictionaryTests<Object>
DictionaryTests<Object>
DictionaryTests<String>
DictionaryTests<String>
DictionaryTests<Guid>
DictionaryTests<Guid>
DictionaryTests<Int32>
DictionaryTests<Int32>
DictionaryTests<Object>
DictionaryTests<Object>
DictionaryTests<String>
DictionaryTests<String>
DictionaryTests<Guid>
DictionaryTests<Guid>
DictionaryTests<Int32>
DictionaryTests<Int32>
DictionaryTests<Object>
DictionaryTests<Object>
DictionaryTests<String>
DictionaryTests<String>
DictionaryTests<Guid>
DictionaryTests<Guid>
DictionaryTests<Int32>
DictionaryTests<Int32>
DictionaryTests<Object>
DictionaryTests<Object>
DictionaryTests<String>
DictionaryTests<String>
DictionaryTests<Guid>
DictionaryTests<Guid>
DictionaryTests<Int32>
DictionaryTests<Int32>
DictionaryTests<Object>
DictionaryTests<Object>
DictionaryTests<String>
DictionaryTests<String>
DictionaryTests<Guid>
DictionaryTests<Guid>
DictionaryTests<Int32>
DictionaryTests<Int32>
DictionaryTests<Object>
DictionaryTests<Object>
DictionaryTests<String>
DictionaryTests<String>
DictionaryTests<Guid>
DictionaryTests<Guid>
DictionaryTests<Int32>
DictionaryTests<Int32>
DictionaryTests<Object>
DictionaryTests<Object>
DictionaryTests<String>
DictionaryTests<String>
DictionaryTests<Guid>
DictionaryTests<Guid>
DictionaryTests<Int32>
DictionaryTests<Int32>
DictionaryTests<Object>
DictionaryTests<Object>
DictionaryTests<String>
DictionaryTests<String>
DictionaryTests<Guid>
DictionaryTests<Guid>
DictionaryTests<Int32>
DictionaryTests<Int32>
DictionaryTests<Object>
DictionaryTests<Object>
DictionaryTests<String>
DictionaryTests<String>
DictionaryTests<Guid>
DictionaryTests<Guid>
DictionaryTests<Int32>
DictionaryTests<Int32>
DictionaryTests<Object>
DictionaryTests<Object>
DictionaryTests<String>
DictionaryTests<String>
DictionaryTests<Guid>
DictionaryTests<Guid>
DictionaryTests<Int32>
DictionaryTests<Int32>
DictionaryTests<Object>
DictionaryTests<Object>
DictionaryTests<String>
DictionaryTests<String>
DictionaryTests<Guid>
DictionaryTests<Guid>
DictionaryTests<Int32>
DictionaryTests<Int32>
DictionaryTests<Object>
DictionaryTests<Object>
DictionaryTests<String>
DictionaryTests<String>