C#+WPFチューニング戦記

C#とWPFで高速なコードと最適なシステムを書くためにやってきたいろいろな事を書いてみます。.NET Frameworkのソースコードを読み解きましょう。なお、ここに書かれているのは個人の見解であって何らかの団体や企業の見解を代表するものではありません。

DependencyPropertyの急所を見つける

WPFに触れたことのある人ならバインディングの便利さを知らぬ人は居ないでしょう。これはその骨にあたる部分の話です。

 

DependencyObjectを継承したクラスにはDependencyPropertyを使用することができます。これは通常の依存関係プロパティとしてでなく、添付プロパティ、継承プロパティなどにも使用することができます。

そのほか値の有効性の判定や、変更時のコールバックやイベントなどの機能。デフォルト値から変更しなければメモリが節約されること、メタデータの設定に応じて様々な付加機能が使えるところなど、究極的に便利なプロパティです。本当に手厚いものです。

便利である反面、以下のような分かり易いボトルネックを持っています。

  1. DependencyPropertyは内部の情報保持はObjectであるため、値型を格納する際はボックス化、アンボックス化のコストを必ず持ちます。つまり、GCがフル稼働する要因となります。
  2. DependencyObject自体の構造はクラスインスタンスとプロパティ名(実際はそのプロパティ名に付された番号)に紐付いた辞書としてグローバルに配置されます。まず、GetHashCode()とEquals()がDependencyObjectでsealedされていることから、このDependencyPropertyはこの部分の出来に相当依存していることがうかがい知れます。
  3. DependencyPropertyの重要な要素、PropertyMetadataにはいくつかの姉妹版があります。FrameworkPropertyMetadataやUIPropertyMetadataなどです。
    それぞれ、複雑さが異なります。FrameworkPropertyMetadataなど、この複雑さを知ってしまうと使うのにためらいが生じるほどです。
    最も安価なのはPropertyMetadataです。

あと、基本的な事として、単純なGetterとSetterのコストを理解しておくのは良いでしょう。

Getterの動作

  1. クラスインスタンスと変数名(変数名は厳密には登録番号)の辞書からObject返します。これが速さを期待できない程度に高負荷の処理であることは想像できるでしょう。また、値型の場合はボックス化されています。
  2. 一般プロパティでGetValueを用いているなら、戻ってきた値はキャスト(値型の場合はアンボックス化)してプロパティの型に合わせて返します。

Setterの動作

  1. 設定しようとしている値がデフォルト値と同じ場合は値を削除してデフォルト値に戻して終了します。
  2. 値の正当性チェックを行います(この処理が実装されている場合、通常はアンボックス化が行われます)。

  3. Getterの1の動作を行い、現在値を取得します。
  4. 比較を行い、変更が必要ではない場合、ここで処理を中断します。

  5. 変更が生じた場合、辞書を書き換えます。
  6. PropertyMetadataの種類(FrameworkPropertyMetadata、UIPropertyMetadataなど)に応じた更新処理を経ます。
  7. PropertyMetadataのPropertyChangedCallbackを呼び出します。
  8. DependencyObjectのOnPropertyChangedを呼び出します。

どこをどうやっても、これだけの処理はSetValueとGetValue周辺で実行されます。Setter側はバインディングされている場合に直接SetValueが呼ばれてしまうため、もはや省略のしようもありません。

となれば、ここを軽量化するにはこのように書くしかありません。

class Hoge : DependencyObject

{

    private int _foo = FooProperty.DefaultMetadata.DefaultValue;

    public int Foo {

        get { return _foo; }

        set { SetValue( FooProperty, value ); }

    }

    static readonly DependencyProperty FooProperty = DependencyProperty.Register( ”Foo", typeof(int), typeof(Hoge), (d,e)=>{ ((Hoge)d)._foo = e.NewValue; }); 

}

この方法は、バインディングのみで使用するコードには効果がありませんので、コードから直接参照されるプロパティに限るものとします。

また、コーディングする側の原則論としても、普通のプロパティのように気軽にSetterを使用しないことを意味しています。

また、ここまで書いておきながら、_fooのキャッシュを持つことはメモリの無駄という考え方も存在します。少なくとも、グローバルな依存関係プロパティ辞書とクラス内に同一内容の情報を保持することにはなります。

それでも、コード側からの参照がある場合はこの手法は高速化に大きな効果があります。速度とメモリのどちらを選ぶか、それは業務上の様々なシーンによって異なってくるでしょう。

また個人的には、ジェネリックで、ボックス化に正しい配慮の為されたPropertyMetadataがあると、かなり速くなるのではないかと考えています。