1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

概要

C# には標準のキーと値のコレクション System.Collections.Generic.Dictionary<TKey,TValue> があります。

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

ここで keysvaluesSpan<T> で取りたい、というのが今回の話です。

ちなみに

キーまたはインデックスからアクセスできるキーと値のペアのコレクションが欲しい場合は .net9 preview6System.Collections.Generic.OrderedDictionary<TKey, TValue> が追加されたので、そちらを利用できます。

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) で2桁くらい遅いので、基本的にはおまけです。

// 使い方
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);

コード

SpanDictionary
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);
    }
}
テストコード
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();
        };
    }
}

パフォーマンス

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() が非常に遅いので、使い物になりません。
  • なんか全体的にパフォーマンス悪いねェ・・・
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?