DIにおけるリフレクション情報のキャッシュ(1

ここしばらくは、.NETスマートクライアントでDIコンテナを使う際の省力化、効率化、性能向上を目的としたエントリをテーマにしていたが、先日のAOPネタ他でそれも大体は目処がついた。以下、ネタにしてきた内容の一覧だ。

もうやることは無いと思ったのだが、一つだけひっかかっていることがある。それは、私がDIコンテナの自動バインド処理、自動登録処理は重い処理だ、と書いたことに対してのひがさんのコメントだ。

プロパティのリフレクション情報をキャッシュしてます?

キャッシュしてれば、それほど変わらない気がするんですけど

前にも書いたが、拙作のDIコンテナはSeasar2のような型のリフレクション情報をキャッシュするようなことはしていない。一番の理由は.NETのリフレクション情報の取得はJavaに比べて軽いということだ。だから型の情報が欲しい場合には、以下のように直接Typeクラスからリフレクション情報を取得していた。

//Foobarクラスにおける、プロパティ"Hoge"の情報を取得する
PropertyInfo propertyInfo = typeof(Foobar).GetProperty("Hoge");
//Foobarクラスにおける、フィールド"hoge"の情報を取得する
FieldInfo fieldInfo = typeof(Foobar).GetField("hoge");

しかし、そもそもコンポーネントの自動バインド処理、自動登録処理の性能に不満があると書いたのは自分であり、ならば、本家Seasar2がリフレクション情報のキャッシュにより、その性能を上げているというのであれば試してみるしかあるまい、と思い、今回のDIネタのまとめとして実装してみることにした。

  • リフレクション情報の入れ物を用意する

Seasar2では"BeanDesc"と呼ばれているインタフェースだ。.NETではBeanという用語はあまり馴染みが無いので、"TypeDesc"という名前で、同様の目的のインタフェースを考えてみた。

public interface ITypeDesc
{
    Type Type { get; }
    bool HasProperty(string propertyName);
    PropertyInfo GetPropertyInfo(string propertyName);
    PropertyInfo GetPropertyInfos(Predicate match);
    bool HasField(string fieldName);
    bool HasConstantField(string fieldName);
    FieldInfo GetFieldInfo(string fieldName);
    FieldInfo GetConstantFieldInfo(string fieldName);
    FieldInfo GetFieldInfos(Predicate match);
    FieldInfo GetConstantFieldInfos(Predicate match);
    bool HasMethod(string methodName);
    MethodInfo GetMethodInfos(Predicate match); 
    ConstructorInfo GetConstructorInfos(Predicate match);
    Attribute GetCustomAttributes(Predicate match);
}

特徴的なのはプロパティ情報や、メソッド情報のようにリフレクションの情報が配列で戻る操作に対して.NET 2.0特有のPredicateデリゲートを指定できるようになっていることだ。STLなどを知っている開発者であれば解ると思うが、これは式の評価が基準を満たしているかをブール値で返すためのデリゲートであり、通常は匿名デリゲートでブロックとして記述する。

例) 書き込み可能なプロパティの情報だけを取得する

ITypeDesc typeDesc = TypeDescFactory.GetTypeDesc(componentType);
PropertyInfo[] propInfos = typeDesc.GetPropertyInfos(
    delegate(PropertyInfo info)
    {
        return (info.CanWrite);
    });

リフレクション情報は一意で使うこともあるが、多いのはこのように、複数ある情報の中から条件に合致する情報だけを対象にして処理を繰り返すことなので、条件式ブロックを外部に委譲できるPredicateは非常に便利なのである。(C#3.0になってラムダが使用可能になったらもっとすっきり書けるだろう)
あとは、このインタフェースを単純に実装するクラスを書くだけだ。実装は簡単なので省略するが、基本的には最初に型からリフレクション情報を取得して、ITypeDescインタフェースで必要な情報をクラス内部に格納しておくだけだ。特徴的なPredicateデリゲートをパラメタに指定できるメソッドは以下のように、全てArrayクラスで実装されている。

public PropertyInfo[] GetPropertyInfos(Predicate match)
{
    return Array.FindAll(this.propertyInfos, match);
}

なお、複数のリフレクション情報をフィルタする際は上記のPredicateで良いのだが、プロパティ名などでリフレクション情報を一意で決めうちしたい場合に同様の処理を行うと、遅くなる可能性がある。配列に対するフィルタは単にループ処理で合致を検査しているだけだからだ。それを避けるために、一意で検索するような処理、つまり上記のITypeDescインタフェースにおけるHas〜メソッドやGet〜Infoメソッドの実装にはDictionaryを使用している。

例) プロパティ情報をプロパティ名で一意に取得する

private IDictionary properties = new Dictionary();

public PropertyInfo GetPropertyInfo(string propertyName)
{
    return this.properties[propertyName];
}

当然だが、初期処理で全てのプロパティ情報を取得した際に、Dictionaryにもプロパティ情報を流し込んでおくことが必要になる。

  • リフレクション情報をキャッシュする

ここまでで、リフレクション情報の格納は出来るようになった。あとは、二度目からのリフレクション取得にかかるオーバヘッドを除去するために、一度取得、格納した情報をキャッシュしておくだけだ。
キャッシュのための方法はいろいろあるが、そんな凝ったものはいらないので単純にITypeDescインタフェースを型(Type)毎に保持しておくだけのものにした。つまり、同じ型のITypeDescインタフェースは全て同一のインスタンスを使うということだ。そこで、以下のようなSeasar2同様のファクトリを用意した。

ublic static class TypeDescFactory
{
    private static IDictionary typeDescMap = new Dictionary();

    public static ITypeDesc GetTypeDesc(Type type)
    {
        if (typeDescMap.ContainsKey(type))
        {
            return typeDescMap[type];
        }
        else
        {
            lock (typeDescMap)
            {
                if (typeDescMap.ContainsKey(type))
                {
                    return typeDescMap[type];
                }
                else
                {
                    ITypeDesc desc = new TypeDescImpl(type);
                    typeDescMap.Add(type, desc);
                    return desc;
                }
            }
        }
    }
}

ここまでの作業が完了したら、あとはこの新しいITypeDescインタフェースを、現在のDIコンテナ中でリフレクションを使用しているコード箇所に対して置換、適用していくだけである。置換した殆どはコンポーネントを生成して組み立てる処理とインジェクション(バインド処理)に集中していた。

これで準備は整った。実際にDIコンテナを使用したアプリケーションを動かして、キャッシュの有無による性能差がとれだけあるかを検証してみよう。テストに使うアプリケーションは以前の日記で使用した簡単なWindowsFormsアプリケーション"AutoBindingTest"であり、最後に計測した起動時間をリフレクション情報のキャッシュ無しの速度とした。

                                     1回目   2回目
                                                                                                    • -
リフレクション情報キャッシュ無し : 1328ms, 1344ms リフレクション情報キャッシュ有り : 1359ms, 1297ms

結果だが、殆ど差が無い。差が出た2回目でも50ms程度であり誤差に近い差でしかない。このような結果になった原因は二つ考えられる。

  • .NETのリフレクション情報の取得処理は元々軽いので、キャッシュしても有意差はでない
  • キャッシュのメリットがデメリットに相殺されている

当たり前だがキャッシュにはメリットだけではなく、デメリット(キャッシュする情報を一括格納する処理のオーバヘッド等)もあるため、もう少し検証が必要だろう。