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

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

続・ハッシュコードをバラけさせる意味はあるのか

というわけで、.NET Frameworkのご本尊様を調べてみたのですが。

http://referencesource.microsoft.com/#mscorlib/system/collections/generic/dictionary.cs,290

このコードを見る限り、GetHashCode()は別にバラケさせる必要はありませんね。
コスト比皆無です。
インクリメントで十分じゃありませんか。むしろ、インクリメント確実じゃないですか。

剰余で負の値が重複することを恐れて0x7fffffff論理積とか正味31bitとか、いろいろはっきりしました。

ビット演算は別に遅いとは言いませんが、uintへのキャストの方が最適化時に高速ではないのでしょうか・・・。
自分がレビュワーならそう指摘を書きます。

Dictionaryみたいな凄い使用頻度を持つクラスでこういうこともあります。

ハッシュコードをバラけさせる意味はあるのか

随分間が空きました。やっとプロジェクトが節目っぽいので再開します。

ハッシュコードについてよく言われることがあります。また、条件と言われるものがあります。

  1. 同じインスタンスは常に同じ値を返さなければならない
  2. 異なるインスタンスが同じ値を返すことはあり得るし許容されるが効率は落ちる
  3. ハッシュ値は極力バラけた値になるのが望ましい

最初の2つは意味がよくわかるんですが、最後のがどうもよくわからなかったりしています。
インスタンスごとにインクリメントするのではいけないのでしょうか。
ハッシュテーブル自体も要素数に応じた拡張が行われますが、確率的な話をしてみるとハッシュ値がバラけていることより(偶然同一値を返す事が有りえますし)、1回定めたユニーク値を返せることに注力した方が良くはないか?

等と思ったので今のハッシュはインスタンスごとに、インクリメントして値を保持してます。
GetHashCode()で変に演算するより速いし、ハッシュテーブルの辞書引きも目立った速度劣化が見当たらない気がするし。

何が気になるって、 GetHashCode()で、一生懸命に素数で乗算したりする意味を問いたい感じです。異種混合のハッシュテーブル作るなら意味がありますが。

.NET Frameworkのことだけでも調べておこうかな。

これも課題か。

イベント集約という手法

一長一短あります。
本日はイベント集約のこと。

イベントは大変便利ですが、1つのイベントにどの程度のリスナーが居るかを正確に管理するのは大切です。

WPFを手本にすると、バインディングパスとレイアウトパスに関して見事なイベント集約がなされていることに気づきます。

依存関係プロパティを書き換えるとバインディングは非同期で最後の変更だけが伝わります。

バインディングが終わると、メタデータのオプションや、計測と配置の無効化フラグを見ながら、極力1回ずつだけレイアウトパスが走ります。
特定の処理が終わるまで、イベントの有無だけを蓄積しておく。これが大雑把にイベント集約というものです。

使いこなすと最適化の効果は絶大。

ですが、イベント集約はパスの順序について明確な設計を要します。また、集約したイベントが正確性を欠くと大変なことに。
今日もそのバグで大変なことに。

foreachのILとループのパターン

糖衣構文としてとても有名なforeachはおおまかに2種類のILになる可能性があります。

  1. 配列に対するIL(for文とほぼ同等)
  2. IEnumerable になる IL

ループだけの速度では前者が約2倍速です。
IEnumerator が IDisposable なので IEnumerableは try - finally で囲われ、finally内でDisposeが呼ばれる・・・・ということになります。
実態の型に合わせて最適なコードを選ぶような積極的高速化は行われません。
foreachに渡す物が配列であるか、他のIEnumerableであるかだけが判断材料です。
過度な期待はできません。

さて、最適化というところに主眼を置いてC#を扱う場合、ループを組むときにはおおまかに3通りのパターンがあります。
LINQの内部実装について書いた際に扱ったことがありますが以下の3つです。

  1. 配列
  2. List<T> 長さが分かる(且つインデクサが速い)コレクション(←2014/12/6 修正)
  3. IEnumerable<T> 長さが分からないものの列挙

C#コンパイラに、型判別と最適なループに展開する属性とかを渡せたらいいのですが。
まあ、この属性を乱用したらループの都度コードが3倍になるわけで、場合によってはキャッシュヒット率の低下などかえって有害。素人にはお勧めできない仕様になってしまいますが。

あと、yield return の展開、次のバージョンではもう少しマシになってるんでしょうか。
まだ確認はしていませんが、期待と不安でいっぱいです。

体調不良の原因

昨日まで元気だったのに今日突然熱が出て(最近多い)、ちょっと生活を振り返ってみました。

食生活はちょっと多いけど普通。酒量もさほどでもなし。(本人の主観によるものです。)
睡眠はやや不足気味。
他の色々はようやくピークを越えて落ち着き始めたところ。

それで、リラクゼーションなどを考え、先月からマッサージやストレッチに通い始めているのですが、どうもこの発熱は揉み返しというやつではないだろうかと思い始めています。

そういえば、マッサージに行った後に多いような・・・。

簡素な仮想化パネル

こんなXAML

<Window x:Class="Test.FastCanvas.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:Test.FastCanvas"
        Title="MainWindow" Height="350" Width="525">
    <ScrollViewer x:Name="scroller" HorizontalScrollBarVisibility="Visible">
        <local:FastCanvas x:Name="canvas" />
    </ScrollViewer>
</Window>

こんなコード(usingは省略してます)

namespace Test.FastCanvas
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            for (int y = 0; y < 1000; y++)
            {
                for (int x = 0; x < 100; x++)
                {
                    var element = new Ellipse() { Width = 32, Height = 32, Fill = Brushes.LightPink };
                    element.MouseEnter += (s,e) =>{ ((Ellipse)s).Fill = Brushes.Blue; };
                    element.MouseLeave += (s, e) => { ((Ellipse)s).Fill = Brushes.LightPink; };
                    Canvas.SetLeft(element, x * 32);
                    Canvas.SetTop(element, y * 32);
                    canvas.AddElement(element);
                }
            }
            canvas.Height = 32 * 1000;
            canvas.Width = 32 * 100;
            scroller.ScrollChanged += scroller_ScrollChanged;
        }

        void scroller_ScrollChanged(object sender, ScrollChangedEventArgs e)
        {
            var rect = new Rect(
                scroller.HorizontalOffset, 
                scroller.VerticalOffset, 
                scroller.ViewportWidth, 
                scroller.ViewportHeight);

            canvas.SetViewport(rect);
        }
    }
}

こんなコントロール(usingは省略してます)

namespace Test.FastCanvas
{
    public class FastCanvas : Canvas
    {
        private HashSet<UIElement> _virtualChildren = new HashSet<UIElement>();

        public void AddElement(UIElement element)
        {
            _virtualChildren.Add(element);
        }

        public void RemoveElement(UIElement element)
        {
            _virtualChildren.Remove(element);
        }

        public void SetViewport(Rect rect)
        {
            foreach(FrameworkElement child in _virtualChildren)
            {
                var childRect = new Rect(Canvas.GetLeft(child), Canvas.GetTop(child), child.Width, child.Height);
                if (!rect.IntersectsWith(childRect))
                {
                    if (Children.Contains(child))
                        Children.Remove(child);
                }
                else
                {
                    if (!Children.Contains(child))
                        Children.Add(child);
                }
            }
        }
    }
}

分かり易く仮想化を説明する教材として作ったものです。
ScrollChangedを使うと古風なので(あとレイアウトパスが過剰に走って遅いので)、データバインディングでViewportを設定できるようにするとMVVM風ですよね。マルチバリューコンバーターの出番です。

起動時間と、マウスヒットテストは仮想化の有無だけで相当違います。それを体感するためのサンプルと思っていただけたら。
ちなみに、同じコードでFastCanvasをCanvasに入れ替え、ScrollChangedのイベントをやめるだけで、仮想化自体の威力は体感できると思います。

実は最近、Visualを細切れにしたらいいんじゃないかと思い

最近まで、よりも速い仮想化パネルを作るということに心血注いでいて一段落ついたところなのですが、まだいくつかやり残したことがあります。

というのは、あるチューニング中の出来事で、再描画領域がパネルの左上から右下まで突き抜けているベジェを変更するときに、極限まで再描画範囲を削ってみたら、画面にWPFらしからぬごみが残る現象が発生しました。

そこでふと思ったのです。
ビジュアルツリー自体は配下のどの範囲が書き換えられるかというところにはすごく厳しいのですが、じゃあ、隣接域を侵害しないように細切れに物を配置してみたらどうだろうと。
仮想化としては、その細切れを根元から着脱・・・・。

もしかして、今よりもっと速い方法につながりうるのではないかと思ったのです。

ただ、用心が必要なのは、見かけのコントロール1つが細切れになって10くらいのVisualになれば、メモリは消費する可能性が高いし、・・・・どっちがいいんでしょう。

次回の開発に入る前に、1日くらい時間を取らせてもらって、そういう実装の速度を試験してみることも必要かなと思った次第です。

ペンはブラシに負けます

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

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

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層をどこまで綺麗に書いても)、ビジュアルツリーの制御が速くならないことには全くもって速くならないということが分かるでしょう。