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

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

ペンはブラシに負けます

ペンは仕掛けが複雑なので、図形を書く際には縁が要らないならブラシだけで書きましょう。場合によっては、四角形を回転拡縮して描いた方が速い事すら有ります。やや極端ですが。

余談ですが、最近短いのが多いのは、スマホ端末から書いているからです。

VisualBrushの恐怖を一言で表現する

Visualの描画範囲が、Direct3Dのテクスチャの最大サイズを超える場合、滲みます。*1

*1:中間バッファがレンダリング命令ではなく、レンダー結果のテクスチャだからです。MSDNの表現だけではそれを汲み取りにくいんですよね。印刷の時とかご注意。

共変性と反変性のこと、無法なobjectを撤廃する事

C#に触れていると嫌でも出てくる共変性と反変性のこと。
これを合理的に扱えずに悩んだことはありますか?
そんなあなたにC#的処方。

あんまり、ここしばらくリファレンスコードのことばかり書いたので、少しは普通のコードを論理的に綺麗にまとめる手法を提案したいと考えています。

interface IRecyclableItemCore
{
    void Release();
}

interface IRecyclableItem<in T> : IRecyclableItemCore
{
    void Initialize<T>(T param);
}

class RecyclableElement : FrameworkElement, IRecyclableItem<ViewModel>
{
    // 中身のことは後日
}

// さて、これを使うケースとは何でしょう。

これがどんな局面で役立つか、分かる人は鋭いと思います。一見、ただのインターフェース継承ですが、インターフェース継承とジェネリックには特異な利点があります。

  1. 外部から共変性が求められる部分を上記 IRecvclableItemCore のように記述できること
  2. 内部で反変性が求められる部分を上記 IRecyclableItem のように記述できること
  3. ジェネリック型パラメータの利用により、上記の2つの特性を1つのクラスに共存させることができること

共変性と反変性はコードの安全性に欠かせません。これを正しく扱うことは大切です。
このコードの使い方の答えは次回にでも。簡単に言うと、高速仮想化パネルの制作に必要だいうだけです。皆さんはWPFにとって劣悪な条件下でも高速なパネルというキーワードに魅力を感じないでしょうか。

散々速度のこと書いてきましたが、速度を重視するばかりでコードの保守性を損なってはいけません。コードの最速性は重要視しつつ、保守性もしっかりフォローというのが大切です。(今日の目的です)

これを基底のインターフェースとするなら、どんな仮想化クラス、仮想化パネルが生まれるのでしょう。ここから先は物語のような様相を呈しますので、しばしのお時間をいただくことになるかと思います。

一言、DataContextのようにあちこちをobjectで扱うインターフェースを使うのは気持ちの良いものではありません。
正しい方法さえ示されていれば、DataContextですらジェネリックに扱うことができたはずなのですが、そうはなっていません。
そもそも、あらゆる型を受け止めきる、万能のViewなど不可能なのですから。
つまり、Viewが受けるべきViewModelの情報は、なんだかんだでViewの形容にある程度束縛されるのです。少なくともViewは特定名称の依存関係プロパティを欲するのですから。欲しいプロパティをインターフェイスとして要求するのは有りではないかと思います。

なぜDependencyPropertyはジェネリックではないのでしょうか?(その問いに答えがない限り、我々は依然ボックス化とアンボックス化のコストの奴隷です。)

さて、では間違いなく速度が出る、本当のCanvasに話を進めてまいりましょうか。(脱線多々となりそうですが。)

でもその前に一応、Microsoftがこの部分をジェネリックで記述しなかった全体的背景は書く必要がありますね。

数日間休んでいます

社内の勉強会に向けて、ちょっと気張って資料を作っているので、ブログの方を少しだけ休んでいます。
Evernoteに置いてあるネタ帳はまだ多量にあるので、書く時間を見つけては書いていきたいと思っています。

2つの親子関係

タイトルのようなことを書くと、普通はロジカルツリーとビジュアルツリーというのが相場ですが、これは別の話です。
ビジュアルツリーは2つの繋がりを持って初めてビジュアルツリーなのです。

  1. AddVisualChild()で接続される、WPFの描画順序に関わるツリー
  2. VisualChildCountとGetVisualChildでつながる、いわゆるところのビジュアルツリー(通常、overrideするもの)

Panelに慣れてしまうとChildrenがあたかも直接取り持ってくれているように錯覚しがちです。
実は、両方つながっていないとMeasureもArrengeもOnRenderもまともに動きません。

そう、考えてみてください。
Panel.ZIndexで描画順序が決まり、Childrenは接続した順番を維持しているのです。
これはつまり2つの繋がりはまったく別個で、描画順序を入れ替えたいときは、ユーザーコード側からは一度RemoveVisualChildして、表示したい順序で接続しなおす必要があるのです。

WPF側からはMeasureパス、Arrangeパスの順序のためのGetVisualChildとVisualChildCountです。
ちなみにOnRenderはArrangeの中で直接呼ばれています。
しかし、なんとOnRenderは描画命令をバッファリングするだけ。その順序で描画されないという事実を知ったとき、描画順序はWPF内部の親子リレーションシップの順序に固定されているのだと理解しました。
そして、RecomputeZStateはそのリレーションシップを並び替える最大のボトルネックなのです。

でも、この話をすると、Panel.ZIndexに振り回されずにWPFを最適に動かす描画方法のヒントが見えてくるのではないでしょうか。
あと、ロジカルツリーなんてどこまで最適化しても(もっとはっきり書くとViewModel層をどこまで綺麗に書いても)、ビジュアルツリーの制御が速くならないことには全くもって速くならないということが分かるでしょう。

コピペを極力避けるべき理由

プログラマがコピペを忌むべき理由

  1. あなたがコピペしたコードは、他の人が権利を主張した瞬間に、結合しているすべてのコードの公開を求められるものかもしれません。
  2. あなたがコピペしたコードは、別の人がそこにコピペしたもので、実はどこかのOSSライセンスに抵触しているかもしれません。
  3. あなたがコピペしたコードは、社内のコーディング規約に合わないかもしれません。
  4. あなたがコピペしたコードは、メンテナンスされておらず、自分もメンテナンスできないかもしれません。既知の不具合を読む時間はありましたか?
  5. あなたがコピペしたコードは、あなた達が作ろうとするシステムにはオーバースペックで、速度を損なうかもしれません。
  6. あなたがコピペしたコードは、ほんの少し書き換えただけの従妹が存在するかもしれず、コード重複が発生するとメンテナンス性が下がります。
  7. あなたがコピペしたコードは、あなたの技術を上達させることはほとんどありません。

とはいえ、まとまった1つのライブラリはブラックボックスでさえなければ大変有用に機能します。
OSSの力を活用することには肯定的です。
しかし、あなたはそのライセンスと、その機能について理解していることが必要でしょう。
無知であることは常にリスクです。逆に、知っているならばそれは大変な武器を手に入れたことになります。
機能に必要があって導入したものだとしても、中身を知る努力は常に怠らぬことが肝心です。そして、実行結果のベンチマークは必ず取るようにしましょう。

話は少々変わりますが、
SonarQubeというツールの恩恵が最近大変大きいです。
プロジェクト内のFxCop違反コード、重複コードをどんどん撲滅しています。
重複が少ないという事は、キャッシュヒット率が全体的に向上するという意味です。
こういったありがたいシステムを作ってくださる人たちは本当にすごいと思います。

RecomputeZState()を読みます

http://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Controls/Panel.cs#1001

これはPanel.csの一部です。
ZIndexが変更されたり、Panelの子要素が追加される度にこれくらいの処理が行われることを覚えておきましょう。
ただのソートです。が、これが毎回、1変更ごとに行われるとなったら、なかなかの処理量です。
おまけに毎回、子要素数と同回数、添付プロパティアクセスします。
1万くらい置いてあったらどれほどの重量になるかわかるでしょうか。いや、1千でも相当の負荷です。

透過率の変更コストをご存知ですか?

DrawingContextに対する描画では、いろいろコストを考えなければいけないところがあります。
例えば、下記のコードは同じような描画を期待できそうですが、速いのはどちらでしょうか。

こちらは、同じTransformのものをまとめています。

DrawingContext dc;
for(int idx1=0 ; idx1<100 ; idx1++) {
    dc.PushTransform(transform);
    for(int idx2=0 ; idx2<100 ; idx2++) {
        dc.PushOpacity(alpha);
        // 描画処理
        dc.Pop();
    }
    dc.Pop();
}

こちらは、同じ透過率のものをまとめています。

DrawingContext dc;
for(int idx1=0 ; idx1<100 ; idx1++) {
    dc.PushOpacity(alpha);
    for(int idx2=0 ; idx2<100 ; idx2++) {
        dc.PushTransform(transform);
        // 描画処理
        dc.Pop();
    }
    dc.Pop();
}

大抵の場合、10倍以上の違いで後者が高速です*1
一見計算処理が多そうなPushTransformより、グラフィックアクセラレーターに対するFlush処理を行う可能性が高いPushOpacityの方が、計算量を超えた負荷になるのです。
Direct3DOpenGLに馴染みのある人にはごく普通に理解できることと思いますが。

もちろん描画順序から透過率の変更を避けられない状況はあります。しかし、透過率の変更を減らす方向に処理を整理する方が負荷は低いことが多くなります。

*1:ハードウェアレンダリングの支援が受けられない場合は例外ですが

Z順序の問題とCanvasの関係について

見たまま記述からはてな書式に切り替えてみました。
考えてみたらこっちのほうがWikiっぽくて使いやすいし軽快ですね。
なんか書くのに時間がかかると思っていたら、それは編集用のパネルが遅いからだった、というわけです。
テキストボックスが遅いとか、やっぱりいけませんね。

Wikiといえばその昔、某所のブログシステムを書いていた時に簡易Wikiのようなものを実装したことがありました。
いまやいろいろなWeb編集サービスに組み込まれているので珍しくも何ともありませんが、Wiki程度に簡単なテキストマークアップ言語というのは実に好みです。

手早くかける程度に慣れるにはもうちょっとだけ時間がかかりますが、表を書くくらいならすぐにでも書けますし、ありがたいことです。

本題であるところのZ順序の問題がなぜCanvasにとって深刻なのか、書いてみます。
パネルの種類によって、Z順序の問題は無視してよいほどどうでもいいのです。

パネルの種類 子コントロールのZ順序の速度の問題
StackPanel 重ならないから問題ない
WrapPanel 重ならないから問題ない
TabPanel 1枚しか表示しないから問題ない
Grid 重ねない使い方が一般的なのでめったに問題にならないケースが多い
Canvas 重なるし、コントロールが増えることが多いので問題になる
DockPanel 重ならないから問題ない

他にもいろいろパネルはありますが、Canvas以外でZ順序の速度が問題になることは滅多にありません。
だから、CanvasのZ順序だけ再実装してくれないかなぁとつぶやくわけです。

しかし、あまり表面化していないだけで、上記のすべてのパネルはPanelを継承していますので、コントロール数が増えると過激に重くなります。
仮想化も万能ではありません。特にZ順序が問題になってしまったケースにおいてはこれは難儀です。
簡単に言えば、VisualTreeを変更するコスト、もしくはDataContextを付け替えて再バインディングが生じるコストなどが付随します。なかなか万能の処方が無いのが難しいところです。

VirtualizingStackPanelのコード長いですね

ここのところ毎日ソースコードを読みながら実況中継という気分ですが。1日あたり1クラス程度を読むのを日課にしておりますが、本日は苦戦中です。

 

PanelがベースになっているVirtualizingPanelを基底クラスに持ちながら、VirtualizingStackPanelあの量の機能を汎用で実装するにあたり、7789行。

凄いなぁと思ったのはこのあたりでしたね。

http://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Controls/VirtualizingStackPanel.cs#1535

こんなにref・・・・。あ、いやいやこの書き方自体は速いから可ですが。

MeasureOverride周辺は、凄まじい実装の様相です。

読んでみると、標準的なスタックパネルっぽく、仮想化するために大変な苦労をしているところまでは読めます。

StackPanelに、MeasureとArrangeのヘルパーメソッドがあるのも(それがinternalで残念なことも)初めて知りましたし。

Panel系の動きを把握するには、ItemsHostに対する処理を読むのがコツです。

 

そういえば、全く関係ありませんが、ZIndex関連の実装だけはどうしても納得が行かないので、次のバージョンではなんとかしてもらえないだろうかと思います。せめてCanvasだけでも。

せめてPanelはinterface化してもらえるとありがたいと思います。あれを継承するのを拒むと激しい難産になります。WPFの上級セットみたいなものを作るのも楽しそうだと最近は思っています。

手遅れ SizeChanged

SizeChangedのイベントは、ちょっと届くのが遅いと思います。

本当の本当に、レイアウトパスも済んだところで届きます。うっかりこのタイミングで他のコントロールのサイズを上書きしてしまうと、またレイアウトパスが走ります。最悪、バインディングパスも走ります。

SizeChangedイベントを受けてから、それに合わせようとコントロールのサイズや位置を操作するのではなく、あくまでレイアウトパス内(あるいはバインディング、それ以前のユーザーコード)でこれらの処理をしなければ、無駄なレイアウトパスが何度も何度も走る羽目に陥ります。

MeasureとArrangeとUpdateLayoutのリファレンスコードを読んでから、このイベントをどう扱うべきか考えてください。

 

短いコードを速くする努力とは

まずは短く。次いで速く。

そんなお話。

このブログにも実証コードはそのうち掲載していきたいところですが、当分はMicrosoftのリファレンスソースコードにリンクする事を主体としようと考えています。

多くの現場で経験してきた事ですが、使っているものに対する理解は多いほど引き出しが増えるものです。リファレンスコードは宝箱で玉石混交です。目から鱗のコードに出会うこともあれば、目を疑う実装に遭遇もします。

私自身は、公開されているコードを読んだ程度のところもあれば、IL分析までしたものもあれば、実際の機械語で分析したものもあります。

開発という全般を語ると全コードの8割以上は最短で最も読みやすいコードを心がけるべきです。そして残りの部分は、その最短のコードが最速で動く事を心がけることです。
Microsoftは自らのコードにLINQを用いていません。理由は古式な記述より遅いからです。しかし、LINQを可能な限り小さいまま速くする努力を怠っていない事を私達は読む事ができるようになっています。

ところで、公開されているコードだけでは大事なところがexternになってて実体が無く、ところどころ難儀しています。あの部分はどこかに置いてないのでしょうか。

UIElementやFrameworkElementの派生クラスのサイズ

UIElementやFrameworkElementや他のコントロール群。それらのサイズを決めるものは何でしょうか。

WidthやHeightはサイズを決めるものではなく、コントロールの使用者がどのようなサイズを期待しているか、標準の測定ロジック(すなわちMeasure)に知らせるための箱に過ぎません。

サイズを決めるのは、Measureで設定されるDesiredSizeです。そして親パネルのMeasureとArrangeがそれをどのように料理するかです。

これはつまり、UIElementならMeasureCore、FrameworkElementならMeasureOverrideということになります。そこで大胆にWidthやHeightを無視して異なるサイズを設定しても何ら問題は生じません。

MaxWidthやMinWidthすら、標準の測定ロジックの便宜のために存在しているのみです。この周辺を読むと、そのあたりも読むことができます。

http://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/FrameworkElement.cs#4288

http://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/FrameworkElement.cs#4019

 

ただし「独自の測定ロジックを実装する必要」ということは明確なシナリオのイメージが求められます。例えば、そのコントロールは汎用でしょうか、でしたら汎用の測定ロジックを変えないのが望ましいはずです。

特定のパネルの専用コントロール、というシナリオにおいて、おそらくこの専用の測定ロジックというものが活きてくると考えられます。

こうした場合、どのような記述をすればよいでしょうか。

WPF内でもポピュラーな記述方法としては、VisualTreeHelperにより親Visualを取得し、その型をチェックしてから標準ロジックか独自ロジックを選ぶという形になります。

あと、少し興味深いのはMeasureに関してはレイアウトパス外からの使用をしても問題がありません。

このあたりの話はまた別のときに。