Skip to content
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

Merged
merged 3 commits into from Nov 4, 2022
Merged

Conversation

stephentoub
Copy link
Member

@stephentoub stephentoub commented Nov 2, 2022

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))]
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);
}
Type Method Count DefaultComparer Mean
DictionaryTests<Guid> Dictionary_Construct 16 False 308.917 ns
DictionaryTests<Guid> FrozenDictionary_Construct 16 False 2,546.966 ns
DictionaryTests<Int32> Dictionary_Construct 16 False 256.919 ns
DictionaryTests<Int32> FrozenDictionary_Construct 16 False 2,735.513 ns
DictionaryTests<Object> Dictionary_Construct 16 False 459.599 ns
DictionaryTests<Object> FrozenDictionary_Construct 16 False 2,990.372 ns
DictionaryTests<String> Dictionary_Construct 16 False 574.025 ns
DictionaryTests<String> FrozenDictionary_Construct 16 False 3,085.790 ns
DictionaryTests<Guid> Dictionary_TryGetValue_Found 16 False 195.613 ns
DictionaryTests<Guid> FrozenDictionary_TryGetValue_Found 16 False 180.628 ns
DictionaryTests<Int32> Dictionary_TryGetValue_Found 16 False 127.828 ns
DictionaryTests<Int32> FrozenDictionary_TryGetValue_Found 16 False 120.595 ns
DictionaryTests<Object> Dictionary_TryGetValue_Found 16 False 371.343 ns
DictionaryTests<Object> FrozenDictionary_TryGetValue_Found 16 False 384.100 ns
DictionaryTests<String> Dictionary_TryGetValue_Found 16 False 559.943 ns
DictionaryTests<String> FrozenDictionary_TryGetValue_Found 16 False 531.615 ns
DictionaryTests<Guid> Dictionary_TryGetValue_NotFound 16 False 149.167 ns
DictionaryTests<Guid> FrozenDictionary_TryGetValue_NotFound 16 False 120.109 ns
DictionaryTests<Int32> Dictionary_TryGetValue_NotFound 16 False 109.596 ns
DictionaryTests<Int32> FrozenDictionary_TryGetValue_NotFound 16 False 91.319 ns
DictionaryTests<Object> Dictionary_TryGetValue_NotFound 16 False 247.097 ns
DictionaryTests<Object> FrozenDictionary_TryGetValue_NotFound 16 False 231.592 ns
DictionaryTests<String> Dictionary_TryGetValue_NotFound 16 False 356.568 ns
DictionaryTests<String> FrozenDictionary_TryGetValue_NotFound 16 False 334.678 ns
DictionaryTests<Guid> Dictionary_Enumerate 16 False 61.120 ns
DictionaryTests<Guid> FrozenDictionary_Enumerate 16 False 27.948 ns
DictionaryTests<Int32> Dictionary_Enumerate 16 False 50.372 ns
DictionaryTests<Int32> FrozenDictionary_Enumerate 16 False 27.162 ns
DictionaryTests<Object> Dictionary_Enumerate 16 False 89.827 ns
DictionaryTests<Object> FrozenDictionary_Enumerate 16 False 74.392 ns
DictionaryTests<String> Dictionary_Enumerate 16 False 89.446 ns
DictionaryTests<String> FrozenDictionary_Enumerate 16 False 74.626 ns
DictionaryTests<Guid> Dictionary_EnumerateKeys 16 False 49.057 ns
DictionaryTests<Guid> FrozenDictionary_EnumerateKeys 16 False 4.847 ns
DictionaryTests<Int32> Dictionary_EnumerateKeys 16 False 37.000 ns
DictionaryTests<Int32> FrozenDictionary_EnumerateKeys 16 False 5.108 ns
DictionaryTests<Object> Dictionary_EnumerateKeys 16 False 74.497 ns
DictionaryTests<Object> FrozenDictionary_EnumerateKeys 16 False 18.488 ns
DictionaryTests<String> Dictionary_EnumerateKeys 16 False 74.906 ns
DictionaryTests<String> FrozenDictionary_EnumerateKeys 16 False 18.571 ns
DictionaryTests<Guid> Dictionary_EnumerateValues 16 False 38.145 ns
DictionaryTests<Guid> FrozenDictionary_EnumerateValues 16 False 5.054 ns
DictionaryTests<Int32> Dictionary_EnumerateValues 16 False 36.834 ns
DictionaryTests<Int32> FrozenDictionary_EnumerateValues 16 False 5.103 ns
DictionaryTests<Object> Dictionary_EnumerateValues 16 False 54.844 ns
DictionaryTests<Object> FrozenDictionary_EnumerateValues 16 False 5.045 ns
DictionaryTests<String> Dictionary_EnumerateValues 16 False 55.299 ns
DictionaryTests<String> FrozenDictionary_EnumerateValues 16 False 5.048 ns
DictionaryTests<Guid> Dictionary_Construct 16 True 276.233 ns
DictionaryTests<Guid> FrozenDictionary_Construct 16 True 2,480.023 ns
DictionaryTests<Int32> Dictionary_Construct 16 True 247.043 ns
DictionaryTests<Int32> FrozenDictionary_Construct 16 True 2,679.061 ns
DictionaryTests<Object> Dictionary_Construct 16 True 401.734 ns
DictionaryTests<Object> FrozenDictionary_Construct 16 True 2,782.085 ns
DictionaryTests<String> Dictionary_Construct 16 True 360.930 ns
DictionaryTests<String> FrozenDictionary_Construct 16 True 1,385.360 ns
DictionaryTests<Guid> Dictionary_TryGetValue_Found 16 True 116.494 ns
DictionaryTests<Guid> FrozenDictionary_TryGetValue_Found 16 True 124.232 ns
DictionaryTests<Int32> Dictionary_TryGetValue_Found 16 True 87.759 ns
DictionaryTests<Int32> FrozenDictionary_TryGetValue_Found 16 True 50.979 ns
DictionaryTests<Object> Dictionary_TryGetValue_Found 16 True 207.784 ns
DictionaryTests<Object> FrozenDictionary_TryGetValue_Found 16 True 209.461 ns
DictionaryTests<String> Dictionary_TryGetValue_Found 16 True 268.939 ns
DictionaryTests<String> FrozenDictionary_TryGetValue_Found 16 True 142.170 ns
DictionaryTests<Guid> Dictionary_TryGetValue_NotFound 16 True 99.116 ns
DictionaryTests<Guid> FrozenDictionary_TryGetValue_NotFound 16 True 81.180 ns
DictionaryTests<Int32> Dictionary_TryGetValue_NotFound 16 True 72.956 ns
DictionaryTests<Int32> FrozenDictionary_TryGetValue_NotFound 16 True 53.239 ns
DictionaryTests<Object> Dictionary_TryGetValue_NotFound 16 True 163.937 ns
DictionaryTests<Object> FrozenDictionary_TryGetValue_NotFound 16 True 144.715 ns
DictionaryTests<String> Dictionary_TryGetValue_NotFound 16 True 178.219 ns
DictionaryTests<String> FrozenDictionary_TryGetValue_NotFound 16 True 109.456 ns
DictionaryTests<Guid> Dictionary_Enumerate 16 True 58.001 ns
DictionaryTests<Guid> FrozenDictionary_Enumerate 16 True 30.898 ns
DictionaryTests<Int32> Dictionary_Enumerate 16 True 49.878 ns
DictionaryTests<Int32> FrozenDictionary_Enumerate 16 True 27.879 ns
DictionaryTests<Object> Dictionary_Enumerate 16 True 89.847 ns
DictionaryTests<Object> FrozenDictionary_Enumerate 16 True 74.568 ns
DictionaryTests<String> Dictionary_Enumerate 16 True 89.347 ns
DictionaryTests<String> FrozenDictionary_Enumerate 16 True 73.377 ns
DictionaryTests<Guid> Dictionary_EnumerateKeys 16 True 48.112 ns
DictionaryTests<Guid> FrozenDictionary_EnumerateKeys 16 True 5.259 ns
DictionaryTests<Int32> Dictionary_EnumerateKeys 16 True 36.038 ns
DictionaryTests<Int32> FrozenDictionary_EnumerateKeys 16 True 5.016 ns
DictionaryTests<Object> Dictionary_EnumerateKeys 16 True 74.331 ns
DictionaryTests<Object> FrozenDictionary_EnumerateKeys 16 True 19.002 ns
DictionaryTests<String> Dictionary_EnumerateKeys 16 True 73.520 ns
DictionaryTests<String> FrozenDictionary_EnumerateKeys 16 True 18.961 ns
DictionaryTests<Guid> Dictionary_EnumerateValues 16 True 39.389 ns
DictionaryTests<Guid> FrozenDictionary_EnumerateValues 16 True 5.432 ns
DictionaryTests<Int32> Dictionary_EnumerateValues 16 True 37.072 ns
DictionaryTests<Int32> FrozenDictionary_EnumerateValues 16 True 4.999 ns
DictionaryTests<Object> Dictionary_EnumerateValues 16 True 54.973 ns
DictionaryTests<Object> FrozenDictionary_EnumerateValues 16 True 5.033 ns
DictionaryTests<String> Dictionary_EnumerateValues 16 True 54.496 ns
DictionaryTests<String> FrozenDictionary_EnumerateValues 16 True 5.029 ns
DictionaryTests<Guid> Dictionary_Construct 1024 False 20,251.116 ns
DictionaryTests<Guid> FrozenDictionary_Construct 1024 False 165,973.254 ns
DictionaryTests<Int32> Dictionary_Construct 1024 False 16,858.288 ns
DictionaryTests<Int32> FrozenDictionary_Construct 1024 False 149,391.455 ns
DictionaryTests<Object> Dictionary_Construct 1024 False 26,154.444 ns
DictionaryTests<Object> FrozenDictionary_Construct 1024 False 174,545.846 ns
DictionaryTests<String> Dictionary_Construct 1024 False 36,381.752 ns
DictionaryTests<String> FrozenDictionary_Construct 1024 False 200,478.010 ns
DictionaryTests<Guid> Dictionary_TryGetValue_Found 1024 False 16,232.339 ns
DictionaryTests<Guid> FrozenDictionary_TryGetValue_Found 1024 False 14,450.573 ns
DictionaryTests<Int32> Dictionary_TryGetValue_Found 1024 False 10,903.514 ns
DictionaryTests<Int32> FrozenDictionary_TryGetValue_Found 1024 False 9,294.073 ns
DictionaryTests<Object> Dictionary_TryGetValue_Found 1024 False 24,834.107 ns
DictionaryTests<Object> FrozenDictionary_TryGetValue_Found 1024 False 27,807.133 ns
DictionaryTests<String> Dictionary_TryGetValue_Found 1024 False 43,093.660 ns
DictionaryTests<String> FrozenDictionary_TryGetValue_Found 1024 False 39,181.201 ns
DictionaryTests<Guid> Dictionary_TryGetValue_NotFound 1024 False 13,773.527 ns
DictionaryTests<Guid> FrozenDictionary_TryGetValue_NotFound 1024 False 8,281.616 ns
DictionaryTests<Int32> Dictionary_TryGetValue_NotFound 1024 False 10,252.792 ns
DictionaryTests<Int32> FrozenDictionary_TryGetValue_NotFound 1024 False 6,199.940 ns
DictionaryTests<Object> Dictionary_TryGetValue_NotFound 1024 False 20,427.130 ns
DictionaryTests<Object> FrozenDictionary_TryGetValue_NotFound 1024 False 15,131.280 ns
DictionaryTests<String> Dictionary_TryGetValue_NotFound 1024 False 26,598.257 ns
DictionaryTests<String> FrozenDictionary_TryGetValue_NotFound 1024 False 24,474.515 ns
DictionaryTests<Guid> Dictionary_Enumerate 1024 False 3,440.145 ns
DictionaryTests<Guid> FrozenDictionary_Enumerate 1024 False 1,689.686 ns
DictionaryTests<Int32> Dictionary_Enumerate 1024 False 2,520.821 ns
DictionaryTests<Int32> FrozenDictionary_Enumerate 1024 False 1,498.062 ns
DictionaryTests<Object> Dictionary_Enumerate 1024 False 4,408.578 ns
DictionaryTests<Object> FrozenDictionary_Enumerate 1024 False 3,689.598 ns
DictionaryTests<String> Dictionary_Enumerate 1024 False 4,398.440 ns
DictionaryTests<String> FrozenDictionary_Enumerate 1024 False 3,636.864 ns
DictionaryTests<Guid> Dictionary_EnumerateKeys 1024 False 2,359.947 ns
DictionaryTests<Guid> FrozenDictionary_EnumerateKeys 1024 False 261.473 ns
DictionaryTests<Int32> Dictionary_EnumerateKeys 1024 False 2,033.189 ns
DictionaryTests<Int32> FrozenDictionary_EnumerateKeys 1024 False 253.029 ns
DictionaryTests<Object> Dictionary_EnumerateKeys 1024 False 3,742.122 ns
DictionaryTests<Object> FrozenDictionary_EnumerateKeys 1024 False 1,226.121 ns
DictionaryTests<String> Dictionary_EnumerateKeys 1024 False 3,693.788 ns
DictionaryTests<String> FrozenDictionary_EnumerateKeys 1024 False 1,222.912 ns
DictionaryTests<Guid> Dictionary_EnumerateValues 1024 False 2,144.243 ns
DictionaryTests<Guid> FrozenDictionary_EnumerateValues 1024 False 260.725 ns
DictionaryTests<Int32> Dictionary_EnumerateValues 1024 False 2,039.401 ns
DictionaryTests<Int32> FrozenDictionary_EnumerateValues 1024 False 255.137 ns
DictionaryTests<Object> Dictionary_EnumerateValues 1024 False 2,900.210 ns
DictionaryTests<Object> FrozenDictionary_EnumerateValues 1024 False 254.610 ns
DictionaryTests<String> Dictionary_EnumerateValues 1024 False 2,924.998 ns
DictionaryTests<String> FrozenDictionary_EnumerateValues 1024 False 252.582 ns
DictionaryTests<Guid> Dictionary_Construct 1024 True 17,653.020 ns
DictionaryTests<Guid> FrozenDictionary_Construct 1024 True 165,900.270 ns
DictionaryTests<Int32> Dictionary_Construct 1024 True 15,324.775 ns
DictionaryTests<Int32> FrozenDictionary_Construct 1024 True 145,562.184 ns
DictionaryTests<Object> Dictionary_Construct 1024 True 22,357.810 ns
DictionaryTests<Object> FrozenDictionary_Construct 1024 True 164,976.669 ns
DictionaryTests<String> Dictionary_Construct 1024 True 24,584.158 ns
DictionaryTests<String> FrozenDictionary_Construct 1024 True 713,506.562 ns
DictionaryTests<Guid> Dictionary_TryGetValue_Found 1024 True 10,972.369 ns
DictionaryTests<Guid> FrozenDictionary_TryGetValue_Found 1024 True 10,677.337 ns
DictionaryTests<Int32> Dictionary_TryGetValue_Found 1024 True 6,061.961 ns
DictionaryTests<Int32> FrozenDictionary_TryGetValue_Found 1024 True 4,568.878 ns
DictionaryTests<Object> Dictionary_TryGetValue_Found 1024 True 15,521.277 ns
DictionaryTests<Object> FrozenDictionary_TryGetValue_Found 1024 True 15,039.072 ns
DictionaryTests<String> Dictionary_TryGetValue_Found 1024 True 21,252.949 ns
DictionaryTests<String> FrozenDictionary_TryGetValue_Found 1024 True 19,273.593 ns
DictionaryTests<Guid> Dictionary_TryGetValue_NotFound 1024 True 10,827.871 ns
DictionaryTests<Guid> FrozenDictionary_TryGetValue_NotFound 1024 True 6,294.780 ns
DictionaryTests<Int32> Dictionary_TryGetValue_NotFound 1024 True 8,168.398 ns
DictionaryTests<Int32> FrozenDictionary_TryGetValue_NotFound 1024 True 4,152.874 ns
DictionaryTests<Object> Dictionary_TryGetValue_NotFound 1024 True 14,748.903 ns
DictionaryTests<Object> FrozenDictionary_TryGetValue_NotFound 1024 True 9,459.935 ns
DictionaryTests<String> Dictionary_TryGetValue_NotFound 1024 True 20,727.839 ns
DictionaryTests<String> FrozenDictionary_TryGetValue_NotFound 1024 True 11,941.794 ns
DictionaryTests<Guid> Dictionary_Enumerate 1024 True 3,355.945 ns
DictionaryTests<Guid> FrozenDictionary_Enumerate 1024 True 1,676.260 ns
DictionaryTests<Int32> Dictionary_Enumerate 1024 True 2,501.281 ns
DictionaryTests<Int32> FrozenDictionary_Enumerate 1024 True 1,488.353 ns
DictionaryTests<Object> Dictionary_Enumerate 1024 True 4,433.532 ns
DictionaryTests<Object> FrozenDictionary_Enumerate 1024 True 3,662.125 ns
DictionaryTests<String> Dictionary_Enumerate 1024 True 4,485.860 ns
DictionaryTests<String> FrozenDictionary_Enumerate 1024 True 3,652.590 ns
DictionaryTests<Guid> Dictionary_EnumerateKeys 1024 True 2,351.717 ns
DictionaryTests<Guid> FrozenDictionary_EnumerateKeys 1024 True 262.712 ns
DictionaryTests<Int32> Dictionary_EnumerateKeys 1024 True 2,019.313 ns
DictionaryTests<Int32> FrozenDictionary_EnumerateKeys 1024 True 252.664 ns
DictionaryTests<Object> Dictionary_EnumerateKeys 1024 True 3,673.043 ns
DictionaryTests<Object> FrozenDictionary_EnumerateKeys 1024 True 1,224.610 ns
DictionaryTests<String> Dictionary_EnumerateKeys 1024 True 3,748.785 ns
DictionaryTests<String> FrozenDictionary_EnumerateKeys 1024 True 1,228.282 ns
DictionaryTests<Guid> Dictionary_EnumerateValues 1024 True 2,080.066 ns
DictionaryTests<Guid> FrozenDictionary_EnumerateValues 1024 True 255.457 ns
DictionaryTests<Int32> Dictionary_EnumerateValues 1024 True 2,018.673 ns
DictionaryTests<Int32> FrozenDictionary_EnumerateValues 1024 True 255.488 ns
DictionaryTests<Object> Dictionary_EnumerateValues 1024 True 2,872.265 ns
DictionaryTests<Object> FrozenDictionary_EnumerateValues 1024 True 255.872 ns
DictionaryTests<String> Dictionary_EnumerateValues 1024 True 2,895.610 ns
DictionaryTests<String> FrozenDictionary_EnumerateValues 1024 True 254.709 ns

@dotnet-issue-labeler
Copy link

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
Copy link

ghost commented Nov 2, 2022

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))]
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);
}
Type Method Count DefaultComparer Mean
DictionaryTests<Guid> Dictionary_Construct 16 False 308.917 ns
DictionaryTests<Guid> FrozenDictionary_Construct 16 False 2,546.966 ns
DictionaryTests<Int32> Dictionary_Construct 16 False 256.919 ns
DictionaryTests<Int32> FrozenDictionary_Construct 16 False 2,735.513 ns
DictionaryTests<Object> Dictionary_Construct 16 False 459.599 ns
DictionaryTests<Object> FrozenDictionary_Construct 16 False 2,990.372 ns
DictionaryTests<String> Dictionary_Construct 16 False 574.025 ns
DictionaryTests<String> FrozenDictionary_Construct 16 False 3,085.790 ns
DictionaryTests<Guid> Dictionary_TryGetValue_Found 16 False 195.613 ns
DictionaryTests<Guid> FrozenDictionary_TryGetValue_Found 16 False 180.628 ns
DictionaryTests<Int32> Dictionary_TryGetValue_Found 16 False 127.828 ns
DictionaryTests<Int32> FrozenDictionary_TryGetValue_Found 16 False 120.595 ns
DictionaryTests<Object> Dictionary_TryGetValue_Found 16 False 371.343 ns
DictionaryTests<Object> FrozenDictionary_TryGetValue_Found 16 False 384.100 ns
DictionaryTests<String> Dictionary_TryGetValue_Found 16 False 559.943 ns
DictionaryTests<String> FrozenDictionary_TryGetValue_Found 16 False 531.615 ns
DictionaryTests<Guid> Dictionary_TryGetValue_NotFound 16 False 149.167 ns
DictionaryTests<Guid> FrozenDictionary_TryGetValue_NotFound 16 False 120.109 ns
DictionaryTests<Int32> Dictionary_TryGetValue_NotFound 16 False 109.596 ns
DictionaryTests<Int32> FrozenDictionary_TryGetValue_NotFound 16 False 91.319 ns
DictionaryTests<Object> Dictionary_TryGetValue_NotFound 16 False 247.097 ns
DictionaryTests<Object> FrozenDictionary_TryGetValue_NotFound 16 False 231.592 ns
DictionaryTests<String> Dictionary_TryGetValue_NotFound 16 False 356.568 ns
DictionaryTests<String> FrozenDictionary_TryGetValue_NotFound 16 False 334.678 ns
DictionaryTests<Guid> Dictionary_Enumerate 16 False 61.120 ns
DictionaryTests<Guid> FrozenDictionary_Enumerate 16 False 27.948 ns
DictionaryTests<Int32> Dictionary_Enumerate 16 False 50.372 ns
DictionaryTests<Int32> FrozenDictionary_Enumerate 16 False 27.162 ns
DictionaryTests<Object> Dictionary_Enumerate 16 False 89.827 ns
DictionaryTests<Object> FrozenDictionary_Enumerate 16 False 74.392 ns
DictionaryTests<String> Dictionary_Enumerate 16 False 89.446 ns
DictionaryTests<String> FrozenDictionary_Enumerate 16 False 74.626 ns
DictionaryTests<Guid> Dictionary_EnumerateKeys 16 False 49.057 ns
DictionaryTests<Guid> FrozenDictionary_EnumerateKeys 16 False 4.847 ns
DictionaryTests<Int32> Dictionary_EnumerateKeys 16 False 37.000 ns
DictionaryTests<Int32> FrozenDictionary_EnumerateKeys 16 False 5.108 ns
DictionaryTests<Object> Dictionary_EnumerateKeys 16 False 74.497 ns
DictionaryTests<Object> FrozenDictionary_EnumerateKeys 16 False 18.488 ns
DictionaryTests<String> Dictionary_EnumerateKeys 16 False 74.906 ns
DictionaryTests<String> FrozenDictionary_EnumerateKeys 16 False 18.571 ns
DictionaryTests<Guid> Dictionary_EnumerateValues 16 False 38.145 ns
DictionaryTests<Guid> FrozenDictionary_EnumerateValues 16 False 5.054 ns
DictionaryTests<Int32> Dictionary_EnumerateValues 16 False 36.834 ns
DictionaryTests<Int32> FrozenDictionary_EnumerateValues 16 False 5.103 ns
DictionaryTests<Object> Dictionary_EnumerateValues 16 False 54.844 ns
DictionaryTests<Object> FrozenDictionary_EnumerateValues 16 False 5.045 ns
DictionaryTests<String> Dictionary_EnumerateValues 16 False 55.299 ns
DictionaryTests<String> FrozenDictionary_EnumerateValues 16 False 5.048 ns
DictionaryTests<Guid> Dictionary_Construct 16 True 276.233 ns
DictionaryTests<Guid> FrozenDictionary_Construct 16 True 2,480.023 ns
DictionaryTests<Int32> Dictionary_Construct 16 True 247.043 ns
DictionaryTests<Int32> FrozenDictionary_Construct 16 True 2,679.061 ns
DictionaryTests<Object> Dictionary_Construct 16 True 401.734 ns
DictionaryTests<Object> FrozenDictionary_Construct 16 True 2,782.085 ns
DictionaryTests<String> Dictionary_Construct 16 True 360.930 ns
DictionaryTests<String> FrozenDictionary_Construct 16 True 1,385.360 ns
DictionaryTests<Guid> Dictionary_TryGetValue_Found 16 True 116.494 ns
DictionaryTests<Guid> FrozenDictionary_TryGetValue_Found 16 True 124.232 ns
DictionaryTests<Int32> Dictionary_TryGetValue_Found 16 True 87.759 ns
DictionaryTests<Int32> FrozenDictionary_TryGetValue_Found 16 True 50.979 ns
DictionaryTests<Object> Dictionary_TryGetValue_Found 16 True 207.784 ns
DictionaryTests<Object> FrozenDictionary_TryGetValue_Found 16 True 209.461 ns
DictionaryTests<String> Dictionary_TryGetValue_Found 16 True 268.939 ns
DictionaryTests<String> FrozenDictionary_TryGetValue_Found 16 True 142.170 ns
DictionaryTests<Guid> Dictionary_TryGetValue_NotFound 16 True 99.116 ns
DictionaryTests<Guid> FrozenDictionary_TryGetValue_NotFound 16 True 81.180 ns
DictionaryTests<Int32> Dictionary_TryGetValue_NotFound 16 True 72.956 ns
DictionaryTests<Int32> FrozenDictionary_TryGetValue_NotFound 16 True 53.239 ns
DictionaryTests<Object> Dictionary_TryGetValue_NotFound 16 True 163.937 ns
DictionaryTests<Object> FrozenDictionary_TryGetValue_NotFound 16 True 144.715 ns
DictionaryTests<String> Dictionary_TryGetValue_NotFound 16 True 178.219 ns
DictionaryTests<String> FrozenDictionary_TryGetValue_NotFound 16 True 109.456 ns
DictionaryTests<Guid> Dictionary_Enumerate 16 True 58.001 ns
DictionaryTests<Guid> FrozenDictionary_Enumerate 16 True 30.898 ns
DictionaryTests<Int32> Dictionary_Enumerate 16 True 49.878 ns
DictionaryTests<Int32> FrozenDictionary_Enumerate 16 True 27.879 ns
DictionaryTests<Object> Dictionary_Enumerate 16 True 89.847 ns
DictionaryTests<Object> FrozenDictionary_Enumerate 16 True 74.568 ns
DictionaryTests<String> Dictionary_Enumerate 16 True 89.347 ns
DictionaryTests<String> FrozenDictionary_Enumerate 16 True 73.377 ns
DictionaryTests<Guid> Dictionary_EnumerateKeys 16 True 48.112 ns
DictionaryTests<Guid> FrozenDictionary_EnumerateKeys 16 True 5.259 ns
DictionaryTests<Int32> Dictionary_EnumerateKeys 16 True 36.038 ns
DictionaryTests<Int32> FrozenDictionary_EnumerateKeys 16 True 5.016 ns
DictionaryTests<Object> Dictionary_EnumerateKeys 16 True 74.331 ns
DictionaryTests<Object> FrozenDictionary_EnumerateKeys 16 True 19.002 ns
DictionaryTests<String> Dictionary_EnumerateKeys 16 True 73.520 ns
DictionaryTests<String> FrozenDictionary_EnumerateKeys 16 True 18.961 ns
DictionaryTests<Guid> Dictionary_EnumerateValues 16 True 39.389 ns
DictionaryTests<Guid> FrozenDictionary_EnumerateValues 16 True 5.432 ns
DictionaryTests<Int32> Dictionary_EnumerateValues 16 True 37.072 ns
DictionaryTests<Int32> FrozenDictionary_EnumerateValues 16 True 4.999 ns
DictionaryTests<Object> Dictionary_EnumerateValues 16 True 54.973 ns
DictionaryTests<Object> FrozenDictionary_EnumerateValues 16 True 5.033 ns
DictionaryTests<String> Dictionary_EnumerateValues 16 True 54.496 ns
DictionaryTests<String> FrozenDictionary_EnumerateValues 16 True 5.029 ns
DictionaryTests<Guid> Dictionary_Construct 1024 False 20,251.116 ns
DictionaryTests<Guid> FrozenDictionary_Construct 1024 False 165,973.254 ns
DictionaryTests<Int32> Dictionary_Construct 1024 False 16,858.288 ns
DictionaryTests<Int32> FrozenDictionary_Construct 1024 False 149,391.455 ns
DictionaryTests<Object> Dictionary_Construct 1024 False 26,154.444 ns
DictionaryTests<Object> FrozenDictionary_Construct 1024 False 174,545.846 ns
DictionaryTests<String> Dictionary_Construct 1024 False 36,381.752 ns
DictionaryTests<String> FrozenDictionary_Construct 1024 False 200,478.010 ns
DictionaryTests<Guid> Dictionary_TryGetValue_Found 1024 False 16,232.339 ns
DictionaryTests<Guid> FrozenDictionary_TryGetValue_Found 1024 False 14,450.573 ns
DictionaryTests<Int32> Dictionary_TryGetValue_Found 1024 False 10,903.514 ns
DictionaryTests<Int32> FrozenDictionary_TryGetValue_Found 1024 False 9,294.073 ns
DictionaryTests<Object> Dictionary_TryGetValue_Found 1024 False 24,834.107 ns
DictionaryTests<Object> FrozenDictionary_TryGetValue_Found 1024 False 27,807.133 ns
DictionaryTests<String> Dictionary_TryGetValue_Found 1024 False 43,093.660 ns
DictionaryTests<String> FrozenDictionary_TryGetValue_Found 1024 False 39,181.201 ns
DictionaryTests<Guid> Dictionary_TryGetValue_NotFound 1024 False 13,773.527 ns
DictionaryTests<Guid> FrozenDictionary_TryGetValue_NotFound 1024 False 8,281.616 ns
DictionaryTests<Int32> Dictionary_TryGetValue_NotFound 1024 False 10,252.792 ns
DictionaryTests<Int32> FrozenDictionary_TryGetValue_NotFound 1024 False 6,199.940 ns
DictionaryTests<Object> Dictionary_TryGetValue_NotFound 1024 False 20,427.130 ns
DictionaryTests<Object> FrozenDictionary_TryGetValue_NotFound 1024 False 15,131.280 ns
DictionaryTests<String> Dictionary_TryGetValue_NotFound 1024 False 26,598.257 ns
DictionaryTests<String> FrozenDictionary_TryGetValue_NotFound 1024 False 24,474.515 ns
DictionaryTests<Guid> Dictionary_Enumerate 1024 False 3,440.145 ns
DictionaryTests<Guid> FrozenDictionary_Enumerate 1024 False 1,689.686 ns
DictionaryTests<Int32> Dictionary_Enumerate 1024 False 2,520.821 ns
DictionaryTests<Int32> FrozenDictionary_Enumerate 1024 False 1,498.062 ns
DictionaryTests<Object> Dictionary_Enumerate 1024 False 4,408.578 ns
DictionaryTests<Object> FrozenDictionary_Enumerate 1024 False 3,689.598 ns
DictionaryTests<String> Dictionary_Enumerate 1024 False 4,398.440 ns
DictionaryTests<String> FrozenDictionary_Enumerate 1024 False 3,636.864 ns
DictionaryTests<Guid> Dictionary_EnumerateKeys 1024 False 2,359.947 ns
DictionaryTests<Guid> FrozenDictionary_EnumerateKeys 1024 False 261.473 ns
DictionaryTests<Int32> Dictionary_EnumerateKeys 1024 False 2,033.189 ns
DictionaryTests<Int32> FrozenDictionary_EnumerateKeys 1024 False 253.029 ns
DictionaryTests<Object> Dictionary_EnumerateKeys 1024 False 3,742.122 ns
DictionaryTests<Object> FrozenDictionary_EnumerateKeys 1024 False 1,226.121 ns
DictionaryTests<String> Dictionary_EnumerateKeys 1024 False 3,693.788 ns
DictionaryTests<String> FrozenDictionary_EnumerateKeys 1024 False 1,222.912 ns
DictionaryTests<Guid> Dictionary_EnumerateValues 1024 False 2,144.243 ns
DictionaryTests<Guid> FrozenDictionary_EnumerateValues 1024 False 260.725 ns
DictionaryTests<Int32> Dictionary_EnumerateValues 1024 False 2,039.401 ns
DictionaryTests<Int32> FrozenDictionary_EnumerateValues 1024 False 255.137 ns
DictionaryTests<Object> Dictionary_EnumerateValues 1024 False 2,900.210 ns
DictionaryTests<Object> FrozenDictionary_EnumerateValues 1024 False 254.610 ns
DictionaryTests<String> Dictionary_EnumerateValues 1024 False 2,924.998 ns
DictionaryTests<String> FrozenDictionary_EnumerateValues 1024 False 252.582 ns
DictionaryTests<Guid> Dictionary_Construct 1024 True 17,653.020 ns
DictionaryTests<Guid> FrozenDictionary_Construct 1024 True 165,900.270 ns
DictionaryTests<Int32> Dictionary_Construct 1024 True 15,324.775 ns
DictionaryTests<Int32> FrozenDictionary_Construct 1024 True 145,562.184 ns
DictionaryTests<Object> Dictionary_Construct 1024 True 22,357.810 ns
DictionaryTests<Object> FrozenDictionary_Construct 1024 True 164,976.669 ns
DictionaryTests<String> Dictionary_Construct 1024 True 24,584.158 ns
DictionaryTests<String> FrozenDictionary_Construct 1024 True 713,506.562 ns
DictionaryTests<Guid> Dictionary_TryGetValue_Found 1024 True 10,972.369 ns
DictionaryTests<Guid> FrozenDictionary_TryGetValue_Found 1024 True 10,677.337 ns
DictionaryTests<Int32> Dictionary_TryGetValue_Found 1024 True 6,061.961 ns
DictionaryTests<Int32> FrozenDictionary_TryGetValue_Found 1024 True 4,568.878 ns
DictionaryTests<Object> Dictionary_TryGetValue_Found 1024 True 15,521.277 ns
DictionaryTests<Object> FrozenDictionary_TryGetValue_Found 1024 True 15,039.072 ns
DictionaryTests<String> Dictionary_TryGetValue_Found 1024 True 21,252.949 ns
DictionaryTests<String> FrozenDictionary_TryGetValue_Found 1024 True 19,273.593 ns
DictionaryTests<Guid> Dictionary_TryGetValue_NotFound 1024 True 10,827.871 ns
DictionaryTests<Guid> FrozenDictionary_TryGetValue_NotFound 1024 True 6,294.780 ns
DictionaryTests<Int32> Dictionary_TryGetValue_NotFound 1024 True 8,168.398 ns
DictionaryTests<Int32> FrozenDictionary_TryGetValue_NotFound 1024 True 4,152.874 ns
DictionaryTests<Object> Dictionary_TryGetValue_NotFound 1024 True 14,748.903 ns
DictionaryTests<Object> FrozenDictionary_TryGetValue_NotFound 1024 True 9,459.935 ns
DictionaryTests<String> Dictionary_TryGetValue_NotFound 1024 True 20,727.839 ns
DictionaryTests<String> FrozenDictionary_TryGetValue_NotFound 1024 True 11,941.794 ns
DictionaryTests<Guid> Dictionary_Enumerate 1024 True 3,355.945 ns
DictionaryTests<Guid> FrozenDictionary_Enumerate 1024 True 1,676.260 ns
DictionaryTests<Int32> Dictionary_Enumerate 1024 True 2,501.281 ns
DictionaryTests<Int32> FrozenDictionary_Enumerate 1024 True 1,488.353 ns
DictionaryTests<Object> Dictionary_Enumerate 1024 True 4,433.532 ns
DictionaryTests<Object> FrozenDictionary_Enumerate 1024 True 3,662.125 ns
DictionaryTests<String> Dictionary_Enumerate 1024 True 4,485.860 ns
DictionaryTests<String> FrozenDictionary_Enumerate 1024 True 3,652.590 ns
DictionaryTests<Guid> Dictionary_EnumerateKeys 1024 True 2,351.717 ns
DictionaryTests<Guid> FrozenDictionary_EnumerateKeys 1024 True 262.712 ns
DictionaryTests<Int32> Dictionary_EnumerateKeys 1024 True 2,019.313 ns
DictionaryTests<Int32> FrozenDictionary_EnumerateKeys 1024 True 252.664 ns
DictionaryTests<Object> Dictionary_EnumerateKeys 1024 True 3,673.043 ns
DictionaryTests<Object> FrozenDictionary_EnumerateKeys 1024 True 1,224.610 ns
DictionaryTests<String> Dictionary_EnumerateKeys 1024 True 3,748.785 ns
DictionaryTests<String> FrozenDictionary_EnumerateKeys 1024 True 1,228.282 ns
DictionaryTests<Guid> Dictionary_EnumerateValues 1024 True 2,080.066 ns
DictionaryTests<Guid> FrozenDictionary_EnumerateValues 1024 True 255.457 ns
DictionaryTests<Int32> Dictionary_EnumerateValues 1024 True 2,018.673 ns
DictionaryTests<Int32> FrozenDictionary_EnumerateValues 1024 True 255.488 ns
DictionaryTests<Object> Dictionary_EnumerateValues 1024 True 2,872.265 ns
DictionaryTests<Object> FrozenDictionary_EnumerateValues 1024 True 255.872 ns
DictionaryTests<String> Dictionary_EnumerateValues 1024 True 2,895.610 ns
DictionaryTests<String> FrozenDictionary_EnumerateValues 1024 True 254.709 ns
Author: stephentoub
Assignees: -
Labels:

area-System.Collections

Milestone: 8.0.0

@ghost ghost assigned stephentoub Nov 2, 2022
@geeknoid
Copy link
Member

geeknoid commented Nov 2, 2022

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
Copy link
Member

geeknoid commented Nov 2, 2022

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.

stephentoub and 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>
Copy link
Member

@geeknoid geeknoid left a comment

Choose a reason for hiding this comment

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

This looks great.

@krwq
Copy link
Member

krwq commented Nov 4, 2022

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)

Copy link
Member

@krwq krwq left a 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!

@stephentoub stephentoub merged commit 6c5a440 into dotnet:main Nov 4, 2022
@stephentoub stephentoub deleted the frozencollections branch November 4, 2022 19:49
@dotnet dotnet locked as resolved and limited conversation to collaborators Dec 5, 2022
@jeffhandley jeffhandley added the blog-candidate Completed PRs that are candidate topics for blog post coverage label Mar 14, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.Collections blog-candidate Completed PRs that are candidate topics for blog post coverage new-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