はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【C#】TypeがKeyなDictionaryをStatic Type Cachingに置き換えて処理の高速化させる(実験付き)

はじめに

今回はTypeをKeyにとるDictionaryStatic Type Cachingに変えて処理を高速化させる方法について紹介したいと思います。

Static Type Cachingについては以下のスライドのp46~触れられています。
www.slideshare.net

概要

高速化できるDictionaryの条件は、TypeKeyな場合です。

// Static Type Cachingに置き換えられる対象Dictionary
private static readonly Dictionary<Type, int> Dic = new()
{
    {typeof(int), 1},
    {typeof(float), 2},
    {typeof(double), 3}
};

Static Type Cachingに置き換えるには、DictionaryKeyをジェネリックにして、Valueをフィールドにします。

// Static Type Cachingに置き換えたもの
// アプリケーションが終了するまでメモリに存在し続けてしまうことに注意
static class Cache<T>
{
    public static int value;

    static Cache()
    {
        // Tに対してリフレクションで操作したりする場合は静的コンストラクタで一度だけ呼び出す(スレッドセーフで必ず一度しか呼ばれない)
        var t = typeof(T);
        if (t == typeof(int))
        {
            value = 1;
        }
        if (t == typeof(float))
        {
            value = 2;
        }
        if (t == typeof(double))
        {
            value = 3;
        }
    }
}

バイナリサイズを減らす方法

ジェネリックの型ごとに重複したコードがバイナリに含まれるので、コードサイズが大きい箇所はジェネリックの外に切り出した方が良いでしょう。

// Static Type Cachingを利用した場合
// アプリケーションが終了するまでメモリに存在し続けてしまうことに注意
static class Cache<T>
{
    public static int value;

    static Cache()
    {
        // Tに対してリフレクションで操作したりする場合は静的コンストラクタで一度だけ呼び出す(スレッドセーフで必ず一度しか呼ばれない)
        value = CacheHelper.CreateValue(typeof(T));
    }
}

private static class CacheHelper
{
    // Cacheの中に記述するとジェネリックの数だけコードが含まれてしまうため、バイナリサイズが膨らんでしまうので切り出せるところは切り出したい
    // CacheのTが値型 or 参照型で違うらしい(要調査)
    public static int CreateValue(Type t)
    {
        if (t == typeof(int))
        {
            return 1;
        }
        if (t == typeof(float))
        {
            return 2;
        }
        if (t == typeof(double))
        {
            return 3;
        }

        return 0;
    }
}

実験

どれくらい高速化できるか実験してみます。

public class Program
{
    public static void Main(string[] args)
    {
        BenchmarkRunner.Run<Test>();
    }
}

public class Test
{
    // Dictionaryを利用した場合 (<Type, 〇〇>)
    private static readonly Dictionary<Type, int> Dic = new()
    {
        {typeof(int), 1},
        {typeof(float), 2},
        {typeof(double), 3}
    };
    
    // Static Type Cachingを利用した場合 (DictionaryのKeyがTypeのとき代替可能)
    // T毎にインスタンスが生成される
    // アプリケーションが終了するまでメモリに存在し続けてしまうことに注意
    static class Cache<T>
    {
        public static int value;

        static Cache()
        {
            // Tに対してリフレクションで操作したりする場合は静的コンストラクタで一度だけ呼び出す(スレッドセーフで必ず一度しか呼ばれない)
            value = CacheHelper.CreateValue(typeof(T));
        }
    }

    private static class CacheHelper
    {
        // Cacheの中に記述するとジェネリックの数だけコードが含まれてしまうため、バイナリサイズが膨らんでしまうので切り出せるところは切り出したい
        // CacheのTが値型 or 参照型でコードの含まれ方が違うらしい(要調査)
        public static int CreateValue(Type t)
        {
            if (t == typeof(int))
            {
                return 1;
            }
            if (t == typeof(float))
            {
                return 2;
            }
            if (t == typeof(double))
            {
                return 3;
            }

            return 0;
        }
    }
    
    [Benchmark]
    public int UseDictionary()
    {
        return Dic[typeof(int)] + Dic[typeof(float)] + Dic[typeof(double)];
    }
    
    [Benchmark]
    public int UseStaticTypeCaching()
    {
        return Cache<int>.value + Cache<float>.value + Cache<double>.value;
    }
}

結果

Static Type Cachingを利用していた方がかなり高速化していました。(今回の実験では1000倍ほどの差が...)

Method Mean Error StdDev Median
UseDictionary 21.8183 ns 0.4515 ns 0.7788 ns 21.8598 ns
UseStaticTypeCaching 0.0184 ns 0.0187 ns 0.0175 ns 0.0107 ns