メモリ連続の辞書構造を作ってみる

# 概要

C# には標準のキーと値のコレクション [`System.Collections.Generic.Dictionary<TKey,TValue>`](https://learn.microsoft.com/ja-jp/dotnet/api/system.collections.generic.dictionary-2?view=net-8.0) があります。

```cs
var dic = new Dictionary<int, string>();
Dictionary<int, string>.KeyCollection keys = dic.Keys;
Dictionary<int, string>.ValueCollection values = dic.Values;
```

ここで `keys` と `values` を `Span<T>` で取りたい、というのが今回の話です。

## ちなみに

キーまたはインデックスからアクセスできるキーと値のペアのコレクションが欲しい場合は `.net9 preview6` で [`System.Collections.Generic.OrderedDictionary<TKey, TValue>`](https://github.com/dotnet/core/blob/main/release-notes/9.0/preview/preview6/libraries.md#ordereddictionarytkey-tvalue) が追加されたので、そちらを利用できます。

```cs
OrderedDictionary<string, int> d = new()
{
    ["a"] = 1,
    ["b"] = 2,
    ["c"] = 3,
};

d.Add("d", 4);
d.RemoveAt(0);
d.RemoveAt(2);
```

## SpanDictionary

名前は適当です。`Remove()` で要素を削除しても、要素の並びが歯抜けにならず連続になります。  
都合上 `Remove()` は O(n) で**２桁くらい遅い**ので、基本的にはおまけです。

```cs
// 使い方
var dic = new SpanDictionary<int, string>();
dic.Add(1, "one");
dic.Add(5, "five");
dic.Add(3, "three");

ReadOnlySpan<int> keys = dic.Keys;
Assert.Equal([1, 5, 3], keys);

Span<string> values = dic.Values;
Assert.Equal(["one", "five", "three"], values);

dic.Clear();

ref var refOne = ref dic[1];
refOne = "ONE";
Assert.Equal("ONE", dic[1]);

ref var refTwo = ref dic.AddOrRef(2, out var exist);
if (!exist)
    refTwo = "TWO";
Assert.Equal("TWO", dic[2]);

Assert.False(dic.TryGetValue(3, out var three));
Assert.Equal(default, three);

dic.Clear();
dic.Add(1, "one");
dic.Add(2, "two");
dic.Add(3, "three");
dic.Remove(2);

Assert.Equal([1, 3], dic.Keys);
Assert.Equal(["one", "three"], dic.Values);
```

## コード

<details>
<summary>SpanDictionary</summary>

```cs
using System.Diagnostics;

/// <summary>
/// メモリ連続の辞書
/// </summary>
/// <typeparam name="K"></typeparam>
/// <typeparam name="V"></typeparam>
[DebuggerDisplay("Count = {Count}")]
[DebuggerTypeProxy(typeof(DebugView<,>))]
file class SpanDictionary<K, V>
{
    private record struct Entry
    {
        internal int Bucket;
        internal int HashCode;
        internal int Next;
    }

    private static int GetHashCodeKey(in K key) => key!.GetHashCode() & int.MaxValue;

    private Entry[] _entries = Array.Empty<Entry>();
    private K[] _keys = Array.Empty<K>();
    private V[] _values = Array.Empty<V>();

    /// <summary>
    /// キーのスパン
    /// </summary>
    public ReadOnlySpan<K> Keys => this._keys.AsSpan(0, this._count);

    /// <summary>
    /// 値のスパン
    /// </summary>
    public Span<V> Values => this._values.AsSpan(0, this._count);

    private int _count;
    /// <summary>
    /// 要素数
    /// </summary>
    public int Count => this._count;

    /// <summary>
    /// 容量
    /// </summary>
    public int Capacity => this._entries.Length;

    /// <summary>
    /// コンストラクタ
    /// </summary>
    /// <param name="capacity"></param>
    /// <exception cref="System.ArgumentOutOfRangeException"></exception>
    public SpanDictionary(int capacity)
    {
        ArgumentOutOfRangeException.ThrowIfNegative(capacity);
        this.Initialize(capacity);
    }

    /// <summary>
    /// コンストラクタ
    /// </summary>
    public SpanDictionary() : this(0) { }

    /// <summary>
    /// 取得 キーが存在しない場合 default 値を追加 O(1)
    /// </summary>
    /// <param name="key"></param>
    /// <returns></returns>
    public ref V this[in K key] => ref this.Insert(key, false);

    private void Initialize(int capacity)
    {
        int prime = HashUtl.GetPrime(capacity);
        this._entries = new Entry[prime];
        this._keys = new K[prime];
        this._values = new V[prime];
    }

    /// <summary>
    /// 追加 O(1) 既にキーが存在する場合例外
    /// </summary>
    /// <param name="key"></param>
    /// <param name="value"></param>
    /// <exception cref="System.InvalidOperationException"></exception>
    public void Add(in K key, in V value) => this.Insert(key, true) = value;

    /// <summary>
    /// 追加 O(1) 既にキーが存在する場合例外
    /// </summary>
    /// <param name="key"></param>
    /// <returns></returns>
    /// <exception cref="System.InvalidOperationException"></exception>
    public ref V Add(in K key) => ref this.Insert(key, true);

    private void Resize() => this.Resize(HashUtl.ExpandPrime(this._count), false);

    private void Resize(int newSize, bool forceNewHashCodes)
    {
        var entries2 = new Entry[newSize];
        this._entries.AsSpan().CopyTo(entries2);
        var span = entries2.AsSpan(0, this._entries.Length);
        for (int n = 0; n < span.Length; ++n)
            span[n].Bucket = default;

        if (forceNewHashCodes)
        {
            for (int n = 0; n < this._count; n++)
            {
                ref var entry = ref entries2[n];
                if (entry.Next > 0)
                {
                    entry.HashCode = GetHashCodeKey(this._keys[n]);
                }
            }
        }
        for (int n = 0; n < this._count; n++)
        {
            ref var entry = ref entries2[n];
            int num = entry.HashCode % newSize;
            ref var tmp = ref entries2[num];
            entry.Next = tmp.Bucket;
            tmp.Bucket = n + 1;
        }
        this._entries = entries2;

        var keys2 = new K[newSize];
        var keySpan = this._keys.AsSpan(0, this._count);
        keySpan.CopyTo(keys2);
        keySpan.Clear();
        this._keys = keys2;

        var values2 = new V[newSize];
        var valueSpan = this._values.AsSpan(0, this._count);
        valueSpan.CopyTo(values2);
        valueSpan.Clear();
        this._values = values2;
    }

    private unsafe ref V Insert(in K key, bool add, bool* exist)
    {
        int num = GetHashCodeKey(key);
        int num2 = num % this._entries.Length;
        ref var entry2 = ref this._entries[num2];
        var n = entry2.Bucket;
        while (n > 0)
        {
            ref var tmp = ref this._entries[n - 1];
            if (tmp.HashCode == num && EqualityComparer<K>.Default.Equals(this._keys[n - 1], key))
            {
                if (add)
                    throw new InvalidOperationException($"Exist key {key}");
                *exist = true;
                return ref this._values[n - 1];
            }
            n = tmp.Next;
        }
        if (this._count == this._entries.Length)
        {
            this.Resize();
            num2 = num % this._entries.Length;
        }
        ref var entry22 = ref this._entries[num2];
        int num4 = this._count;
        this._count++;
        ref var entry4 = ref this._entries[num4];
        this._keys[num4] = key;
        entry4.HashCode = num;
        entry4.Next = entry22.Bucket;

        entry22.Bucket = num4 + 1;
        *exist = false;
        return ref this._values[num4];
    }

    private unsafe ref V Insert(in K key, bool add)
    {
        bool _ = false;
        return ref this.Insert(key, add, &_);
    }

    /// <summary>
    /// 値を取得 O(1)
    /// キーは新たに作成されない、作成する場合 [] を使用
    /// </summary>
    /// <param name="key"></param>
    /// <param name="value"></param>
    /// <returns></returns>
    public bool TryGetValue(in K key, out V value)
    {
        int num = GetHashCodeKey(key);
        int num2 = num % this._entries.Length;
        ref var entry2 = ref this._entries[num2];
        var n = entry2.Bucket;
        while (n > 0)
        {
            ref var tmp = ref this._entries[n - 1];
            if (tmp.HashCode == num && EqualityComparer<K>.Default.Equals(this._keys[n - 1], key))
            {
                value = this._values[n - 1];
                return true;
            }
            n = tmp.Next;
        }
        value = default!;
        return false;
    }

    /// <summary>
    /// キーが存在する場合 true O(1)
    /// </summary>
    /// <param name="key"></param>
    /// <returns></returns>
    public bool ContainsKey(in K key) => this.TryGetValue(key, out _);

    /// <summary>
    /// 値を取得 O(1)
    /// キーを新たに作成、既に存在している場合 exist は true
    /// </summary>
    /// <param name="key"></param>
    /// <param name="exist"></param>
    /// <returns></returns>
    public unsafe ref V AddOrRef(in K key, out bool exist)
    {
        bool tmp = false;
        ref var result = ref this.Insert(key, false, &tmp);
        exist = tmp;
        return ref result!;
    }

    /// <summary>
    /// クリア
    /// </summary>
    public void Clear()
    {
        if (this._count > 0)
        {
            this._entries.AsSpan().Clear();
            this._keys.AsSpan(0, this._count).Clear();
            this._values.AsSpan(0, this._count).Clear();
            this._count = 0;
        }
    }

    /// <summary>
    /// 要素を削除 O(n)
    /// </summary>
    /// <param name="key"></param>
    /// <returns></returns>
    public bool Remove(in K key)
    {
        int num = GetHashCodeKey(key);
        int num2 = num % this._entries.Length;
        int num3 = 0;
        int n = this._entries[num2].Bucket;
        while (n > 0)
        {
            var n2 = n - 1;
            ref var entry = ref this._entries[n2];
            if (entry.HashCode == num && EqualityComparer<K>.Default.Equals(this._keys[n2], key))
            {
                if (num3 is 0)
                    this._entries[num2].Bucket = entry.Next;
                else
                    this._entries[num3].Next = entry.Next;

                var rightIndex = this._count - 1;
                // コンパクション
                for (int m = n2; m < rightIndex; ++m)
                {
                    ref var left = ref this._entries[m];
                    ref var right = ref this._entries[m + 1];
                    var bucket = left.Bucket;
                    left = right;
                    left.Bucket = bucket;
                    this._keys[m] = this._keys[m + 1];
                    this._values[m] = this._values[m + 1];
                }
                for (int m = 0; m < this._entries.Length; ++m)
                {
                    ref var left = ref this._entries[m];
                    if (left.Bucket > n2)
                        --left.Bucket;
                    if (left.Next > n2)
                        --left.Next;
                }
                ref var last = ref this._entries[rightIndex];
                var bucket2 = last.Bucket;
                last = default;
                last.Bucket = bucket2;
                this._keys[rightIndex] = default!;
                this._values[rightIndex] = default!;

                --this._count;
                return true;
            }
            num3 = n2;
            n = entry.Next;
        }
        return false;
    }
}

file class DebugView<K, V>
{
    private readonly (K, V)[] _array;

    public DebugView(SpanDictionary<K, V> value)
    {
        this._array = new (K, V)[value.Keys.Length];

        for (int n = 0; n < this._array.Length; ++n)
            this._array[n] = (value.Keys[n], value.Values[n]);
    }

    [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
    public (K, V)[] Items => this._array;
}

file static class HashUtl
{
    private static bool IsPrime(int candidate)
    {
        if ((candidate & 1) != 0)
        {
            int num = (int)MathF.Sqrt(candidate);
            for (int i = 3; i <= num; i += 2)
                if (candidate % i == 0)
                    return false;
            return true;
        }
        return candidate == 2;
    }

    internal static int GetPrime(int min)
    {
        for (int j = min | 1; j < int.MaxValue; j += 2)
            if (IsPrime(j) && (j - 1) % 101 != 0)
                return j;
        return min;
    }

    internal static int ExpandPrime(int oldSize)
    {
        int num = 2 * oldSize;
        if (num > 2146435069 && 2146435069 > oldSize)
            return 2146435069;
        return GetPrime(num);
    }
}
```

</details>

<details>
<summary>テストコード</summary>

```cs
using Xunit;

public class _SpanDictionaryTest
{
    void 概要()
    {
        var dic = new Dictionary<int, string>();
        Dictionary<int, string>.KeyCollection keys = dic.Keys;
        Dictionary<int, string>.ValueCollection values = dic.Values;

        OrderedDictionary<string, int> d = new()
        {
            ["a"] = 1,
            ["b"] = 2,
            ["c"] = 3,
        };

        d.Add("d", 4);
        d.RemoveAt(0);
        d.RemoveAt(2);
    }

    [Fact]
    void 使い方()
    {
        // 使い方
        var dic = new SpanDictionary<int, string>();
        dic.Add(1, "one");
        dic.Add(5, "five");
        dic.Add(3, "three");

        ReadOnlySpan<int> keys = dic.Keys;
        Assert.Equal([1, 5, 3], keys);

        Span<string> values = dic.Values;
        Assert.Equal(["one", "five", "three"], values);

        dic.Clear();

        ref var refOne = ref dic[1];
        refOne = "ONE";
        Assert.Equal("ONE", dic[1]);

        ref var refTwo = ref dic.AddOrRef(2, out var exist);
        if (!exist)
            refTwo = "TWO";
        Assert.Equal("TWO", dic[2]);

        Assert.False(dic.TryGetValue(3, out var three));
        Assert.Equal(default, three);

        dic.Remove(1);

        Assert.Equal([2], dic.Keys);
        Assert.Equal(["TWO"], dic.Values);
    }

    [Fact]
    void Keys_Values_Count()
    {
        var dic = new SpanDictionary<int, string>();
        Assert.Equal(0, dic.Count);
        Assert.Equal([], dic.Keys);
        Assert.Equal([], dic.Values);

        dic.Add(1, "one");
        Assert.Equal(1, dic.Count);
        Assert.Equal([1], dic.Keys);
        Assert.Equal(["one"], dic.Values);

        dic.Add(2, "two");
        Assert.Equal(2, dic.Count);
        Assert.Equal([1, 2], dic.Keys);
        Assert.Equal(["one", "two"], dic.Values);
    }

    [Fact]
    void Constructor_Capacity()
    {
        Assert.Throws<ArgumentOutOfRangeException>(() => new SpanDictionary<int, string>(-1));

        {
            var dic = new SpanDictionary<int, string>(10);
            Assert.Equal([], dic.Keys);
            Assert.Equal([], dic.Values);
            Assert.True(dic.Capacity >= 10);
        }
        {
            var dic = new SpanDictionary<int, string>();
            Assert.Equal([], dic.Keys);
            Assert.Equal([], dic.Values);
        }
    }

    [Fact]
    void Indexer()
    {
        var dic = new SpanDictionary<int, string>();
        dic[1] = "one";
        Assert.Equal("one", dic[1]);

        dic[1] = "ONE";
        Assert.Equal("ONE", dic[1]);

        dic[2] = "two";
        Assert.Equal("two", dic[2]);

        dic[3] = "three";
        Assert.Equal("three", dic[3]);
    }

    [Fact]
    void Add_K_V()
    {
        Assert.Throws<InvalidOperationException>(() =>
        {
            var dic = new SpanDictionary<int, string>();
            dic.Add(1, "one");
            dic.Add(1, "one");
        });

        var dic = new SpanDictionary<int, string>();
        dic.Add(1, "one");
        Assert.Equal("one", dic[1]);

        dic.Add(2, "two");
        Assert.Equal("two", dic[2]);

        dic.Add(3, "three");
        Assert.Equal("three", dic[3]);
    }

    [Fact]
    void Add_K()
    {
        Assert.Throws<InvalidOperationException>(() =>
        {
            var dic = new SpanDictionary<int, string>();
            dic.Add(1) = "one";
            dic.Add(1) = "one";
        });

        var dic = new SpanDictionary<int, string>();
        dic.Add(1) = "one";
        Assert.Equal("one", dic[1]);

        dic.Add(2) = "two";
        Assert.Equal("two", dic[2]);

        dic.Add(3) = "three";
        Assert.Equal("three", dic[3]);
    }

    [Fact]
    void 容量が大きくなること()
    {
        var dic = new SpanDictionary<int, string>();
        var capacity = dic.Capacity;
        for (int n = 0; n < capacity + 1; ++n)
            dic.Add(n, n.ToString());

        Assert.True(dic.Capacity > capacity);
    }

    [Fact]
    void TryGetValue()
    {
        var dic = new SpanDictionary<int, string>();
        dic.Add(1, "one");

        Assert.True(dic.TryGetValue(1, out var one));
        Assert.Equal("one", one);

        Assert.False(dic.TryGetValue(2, out var two));
        Assert.Equal(default, two);
    }

    [Fact]
    void ContainsKey()
    {
        var dic = new SpanDictionary<int, string>();
        dic.Add(1, "one");

        Assert.True(dic.ContainsKey(1));
        Assert.False(dic.ContainsKey(2));
    }

    [Fact]
    void AddOrRef()
    {
        var dic = new SpanDictionary<int, string>();
        ref var one = ref dic.AddOrRef(1, out var exist);
        Assert.False(exist);
        one = "one";
        Assert.Equal("one", dic[1]);

        ref var one2 = ref dic.AddOrRef(1, out exist);
        Assert.True(exist);
        one2 = "ONE";
        Assert.Equal("ONE", dic[1]);
    }

    [Fact]
    void Clear()
    {
        var dic = new SpanDictionary<int, string>();
        dic.Add(1, "one");
        dic.Add(2, "two");
        dic.Add(3, "three");

        dic.Clear();
        Assert.Equal(0, dic.Count);
        Assert.Equal([], dic.Keys);
        Assert.Equal([], dic.Values);
    }

    [Fact]
    void Remove()
    {
        var dic = new SpanDictionary<int, string>();
        dic.Add(1, "one");
        dic.Add(2, "two");
        dic.Add(3, "three");

        Assert.True(dic.Remove(2));
        Assert.Equal([1, 3], dic.Keys);
        Assert.Equal(["one", "three"], dic.Values);

        Assert.False(dic.Remove(2));
        Assert.False(dic.Remove(4));
    }

    static (int, string)[] _testData;

    static _SpanDictionaryTest()
    {
        // 固定乱数
        var rand = new Random(0);
        _testData = new (int, string)[10000];
        foreach (ref var n in _testData.AsSpan())
        {
            var num = rand.Next();
            n = (num, num.ToString());
        }
    }

    static Action AddSpanDictionary()
    {
        return () =>
        {
            var dic = new SpanDictionary<int, string>();
            for (int n = 0; n < 10000; ++n)
                dic.Add(n, _testData[n].Item2);
        };
    }

    static Action AddDictionary()
    {
        return () =>
        {
            var dic = new Dictionary<int, string>();
            for (int n = 0; n < 10000; ++n)
                dic.Add(n, _testData[n].Item2);
        };
    }

    static Action AddRandomSpanDictionary()
    {
        return () =>
        {
            var dic = new SpanDictionary<int, string>();
            foreach (var (key, value) in _testData)
                dic.Add(key, value);
        };
    }

    static Action AddRandomDictionary()
    {
        return () =>
        {
            var dic = new Dictionary<int, string>();
            foreach (var (key, value) in _testData)
                dic.Add(key, value);
        };
    }

    static Action RemoveSpanDictionary()
    {
        return () =>
        {
            var dic = new SpanDictionary<int, string>();
            for (int n = 0; n < 10000; ++n)
                dic.Add(n, _testData[n].Item2);
            for (int n = 0; n < 10000; ++n)
                dic.Remove(n);
        };
    }

    static Action RemoveDictionary()
    {
        return () =>
        {
            var dic = new Dictionary<int, string>();
            for (int n = 0; n < 10000; ++n)
                dic.Add(n, _testData[n].Item2);
            for (int n = 0; n < 10000; ++n)
                dic.Remove(n);
        };
    }

    static Action RemoveRandomSpanDictionary()
    {
        return () =>
        {
            var dic = new SpanDictionary<int, string>();
            for (int n = 0; n < 10000; ++n)
                dic.Add(n, _testData[n].Item2);

            var rand = new Random(0);
            for (int n = 0; n < 10000; ++n)
                dic.Remove(rand.Next(0, dic.Count));
        };
    }

    static Action RemoveRandomDictionary()
    {
        return () =>
        {
            var dic = new Dictionary<int, string>();
            for (int n = 0; n < 10000; ++n)
                dic.Add(n, _testData[n].Item2);

            var rand = new Random(0);
            for (int n = 0; n < 10000; ++n)
                dic.Remove(rand.Next(0, dic.Count));
        };
    }

    static Action ForeachSpanDictionary()
    {
        var dic = new SpanDictionary<int, string>();
        for (int n = 0; n < 10000; ++n)
            dic.Add(n, _testData[n].Item2);
        return () =>
        {
            foreach (var n in dic.Values)
                n.GetHashCode();
        };
    }

    static Action ForeachDictionary()
    {
        var dic = new Dictionary<int, string>();
        for (int n = 0; n < 10000; ++n)
            dic.Add(n, _testData[n].Item2);
        return () =>
        {
            foreach (var n in dic.Values)
                n.GetHashCode();
        };
    }
}

```

</details>

## パフォーマンス

| Test                       | Score |         % |  CG0 |
| -------------------------- | ----: | --------: | ---: |
| AddSpanDictionary          |   196 |    100.0% |   66 |
| AddDictionary              |   378 |    192.9% |  148 |
| AddRandomSpanDictionary    |   148 |    100.0% |   48 |
| AddRandomDictionary        |   292 |    197.3% |  161 |
| RemoveSpanDictionary       |     1 |    100.0% |    0 |
| RemoveDictionary           |   401 | 40,100.0% |  200 |
| RemoveRandomSpanDictionary |     1 |    100.0% |    0 |
| RemoveRandomDictionary     |   229 | 22,900.0% |  131 |
| ForeachSpanDictionary      | 1,528 |    100.0% |    0 |
| ForeachDictionary          | 1,295 |     84.8% |    0 |

実行環境: Windows11 x64 .NET Runtime 9.0.0  
**Score** は高いほどパフォーマンスがよいです。  
**GC0** はガベージコレクション回数を表します（少ないほどパフォーマンスがよい）。

- `Remove()` が非常に遅いので、使い物になりません。
- なんか全体的にパフォーマンス悪いねェ・・・