2014年3月19日

型(Type)をキーにした場合の高速なキャッシュ

キャッシュを扱う場合、通常は、Dictionary<TKey, TValue>ConcurrentDictionary<TKey, TValue> などを選択する。

generic 型(Type) を key にする場合のみ、上記クラスを使用するより高速な方法が存在する。
"generic type caching" と呼ばれる方法である。

2015/03/13 追記:
C# 6.0 からは nameof 演算子があるので、EXAMPLE の GetName メソッドは使わないこと。

EXAMPLE

下記メソッドを、キャッシュを使って高速化する場合を考える。

// 最初のプロパティ名を取得する。
// 例のように匿名型と組み合わせることで変数名を文字列として取得可能。
// int value = 0;
// new{value}.GetName() -> "value"
//
public static string GetName<T>(this T source) where T : class
{
    var properties = typeof(T).GetProperties(); //ここが遅い
    return properties[0].Name;
}

  • ConcurrentDictionary の例 ※パフォーマンス測定の結果、キャッシュ無しよりも遅い

public static string GetName<T>(this T source) where T : class
{
    return getNameCache.GetOrAdd(typeof(T), t => t.GetProperties()[0].Name);
}
private static readonly ConcurrentDictionary<Type,string> getNameCache =
    new ConcurrentDictionary<Type,string>();

  • generic type caching の例

public static string GetName<T>(this T source) where T : class
{
    return GetNameCache<T>.Name;
}
private static class GetNameCache<T>
{
    static GetNameCache()
    {
        var properties = typeof(T).GetProperties();
        Name = properties[0].Name;
    }
    public static readonly string Name;
}
generic type caching とは、要は、generic & static なクラスをキャッシュとして使うことである。
型引数が key、static メンバが value に相当する。
静的コンストラクタは、クラスの呼び出し時に1回だけ実行され、スレッドセーフである。
そして、型引数はコンパイル時に解決されるため、Dictionary などの動的な key ルックアップと比べて、圧倒的に早い。

パフォーマンス

[測定コード]

byte ba=0, bb=0;
int ia=0, ib=0;
float fa=0f, fb=0f;
char ca='\0', cb='\0';
string sa=null, sb=null;
var temp = new List<string>(loopCount);

for( var i=0; i<loopCount; i++ ){
    switch( i % 10 ){
    case 0: temp.Add(new{ba}.GetName()); break;
    case 1: temp.Add(new{bb}.GetName()); break;
    case 2: temp.Add(new{ia}.GetName()); break;
    case 3: temp.Add(new{ib}.GetName()); break;
    case 4: temp.Add(new{fa}.GetName()); break;
    case 5: temp.Add(new{fb}.GetName()); break;
    case 6: temp.Add(new{ca}.GetName()); break;
    case 7: temp.Add(new{cb}.GetName()); break;
    case 8: temp.Add(new{sa}.GetName()); break;
    case 9: temp.Add(new{sb}.GetName()); break;
    }
}

[結果]
loopCount キャッシュ無し ConcurrentDictionary generic type caching
10000 0.0054555 0.0190749 0.0039957
100000 0.0270494 0.1461569 0.0086326
1000000 0.2428146 1.4022937 0.0465277
※コマンドプロンプトで実施(csc /optimize)
※10回の算術平均、単位は[s]

結果としては、generic type caching > キャッシュ無し > ConcurrentDictionary となった。(左の方が高性能)
ConcurrentDictionary のルックアップが想定していたよりも遅く、キャッシュ無しの方が早い結果に…。
※一応、Dictionary でも試してみたが ConcurrentDictionary より 2 倍早くなっただけで、キャッシュ無しより遅かった。

キャッシュ無しと比べて generic type caching は、1 万回でも 1.5 [ms] 程しか差は出なかった。
が、共通メソッドとしてできる限りパフォーマンスを良くしたい場合は、導入の価値ありだと思われる。
また、キャッシュの特性として、value の生成処理のコストがより高い場合は、より大きな効果が見込まれる。

まとめ

generic type caching の導入可能条件は下記になる。
  • key が generic 型 (コンパイル時に決定できる Type 型)
  • 削除不要なキャッシュ (一度作成したキャッシュは削除できない)

使いどころとしては、EXAMPLE のようにリフレクション使用時のパフォーマンス向上がメインとなる。
※Type.Get~ や Enum.Get~ など

検証環境

Windows 7 64bit/Visual Studio 2010 SP1/.NET 4.0
Intel(R) Celeron(R) CPU G530 @ 2.40GHz/DDR3 8GB(4GB x 2)

参考URL

0 件のコメント:

コメントを投稿