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

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

値型のデフォルトコンストラクタのコスト

C#における struct は引数が無いコンストラクタを書くことができないのですが、

return new Size();

こういうのはコスト0で戻り値に格納してくれるのだろうかという疑問について、調査しました。
極めて厳密に最適化をすると、このコンストラクタはインライン展開可能で、インライン展開後にさらに最適化すれば戻り値を0フィルするだけでいいので、もしかしたらそのくらいまでなら既存のJITがやってくれるのではないか?という仮説があったためです。

結論から言いますと、現時点 .NET Framework 4.6 では32bit/64bitを問わずJITは言語規約にかなり忠実な展開をします。

つまり、スタック上に1回 0フィルした構造体を作成して戻り値に格納すべく値をコピーします。

最速で書こうと思えば、

static readonly Size _zeroSize = new Size();

:
:

return _zeroSize;

のように記述する必要があります。
こうなると、Size.Emptyが -∞ の値を持つサイズなのが恨めしく映ります。Size.Default も欲しかったなと思います。

ちなみにこの場合も最適化は若干弱いもので、転送元のアドレスをレジスタにロードする処理が含まれます。
デフォルトコンストラクタを用いた値を返す場合、戻り値を直接 0 フィルする最適化が欲しいと思います。
インライン展開時はさらに高度に扱って欲しいと思います。

転送命令1~4個分程度ではありますが、この程度をなぜ最適化できなかったのかという思いはどうしても残ります。

一応、シンプルなコードを作成して最適化の有無に無関係にこのコストはあることをアセンブラ上で確認したつもりではあります。
しかし、コードのパターンによって変わる可能性があることは否めません。
別の答えをお持ちの方がいらっしゃいましたら是非お知らせください。

なお、 new を書かないでも良いものは最適なコードになっています。リテラル値なら良いということなのでしょう。

余談ですが、 default(Size) は new Size() の糖衣構文なので、まったく同じ命令を出力します。

.NET 4.6 hotfix


Issue 4でここ2回の日記で問題にしていた件が解消しているようです。GC内部で長時間フリーズする問題です。

VS2015がフリーズするので

devenv.exe.configにgcServerを有効にする1行を加えたら素晴らしい安定。

hotfix待ちの方々に福音として。

理由はこちらにて。

DLL-hellという言葉を思い出す

きっかけは、VS2015でビルドしたプログラムが30秒もフリーズした事でした。

dotTraceでGCがぶんぶん回ってるのを見て、検索したところ、こんな話に当たりました。

http://stackoverflow.com/questions/32048094/net-4-6-and-gc-freeze

.NET 4.6 のGCのジェネレーション2はフリー領域の探索にバグと言っていいほどの遅い部分があると言うお話になりました。
問題はVS2015ではなく、.NET 4.6とのことです。もしかしてVS2015が時々唐突にフリーズするのもこれが原因では…。

hotfix待ちで良いかという事だけで長い時間議論できそうです。困ってます。

昨日の勉強会

やっと、WPF高速化ネタの一番大きいものをオープンにしました。昨晩の勉強会です。

資料は近日スライドシェア予定です。
ソースコードも公開予定です。

手加減一切無し。ただ高速に動くためのWPFコーディングパターンです。消極的な戦術で逃げ切れない時はどうするかという参考にしていただきたいと思っています。

次はユニバーサルアプリかなぁと思っています。既にいくつか思うことがありまして。

パフォーマンスが落ちていないことを確認する大切な作業

新しい糖衣構文が出てきたらILの品質を確認しましょう。 VisualStudio 2015のC#では以下のように同じようなプロパティを記述できます。

class Test{

   int Value1{ get; set; } = 100;

   int _value2 = 100;
   int Value2 { get{ return _value2; } set{ _value2 = value; } }

   int Value3 { get; set; }

   public Test() {
      Value3 = 100; 
   }
}

ILSpy等を用いて逆アセンブルしてみると、上記の Value1 Value2 Value3 は全く同等のコードであることがわかります。

つまり、安心して最も短い構文を用いてよいことになります。

こうやって、新しい糖衣構文がどのように展開されるか理解することは結構重要です。

何と申しますか、昔の職場の先輩が新製品のCPUが出る都度CPUの仕様書などから速度を検証していたことを思い出します。これはそれと似た作業だと思っています。

デリゲートの最適化にもう1歩踏み込む

デリゲートの最適化についてはもう1歩踏み込んでみたいと思います。

下記のようなコードをビルドしてみると

        /// <summary>
        /// string.IsNullOrWhiteSpace
        /// </summary>
        static Func<string, bool> stringIsNullOrWhiteSpace = string.IsNullOrWhiteSpace;

        static void Main(string[] args)
        {
            Where(stringIsNullOrWhiteSpace, "test");

            Where(string.IsNullOrWhiteSpace, "test");

            Where(x =>string.IsNullOrWhiteSpace(x), "test");
        }

これがこのようになるというわけです。

    private static Func<string, bool> stringIsNullOrWhiteSpace = new Func<string, bool>(string.IsNullOrWhiteSpace);

    private static void Main(string[] args)
    {
        Program.Where(Program.stringIsNullOrWhiteSpace, "test");

        Program.Where(new Func<string, bool>(string.IsNullOrWhiteSpace), "test");

        Func<string, bool> arg_4B_0;
        if ((arg_4B_0 = Program.<>c.<>9__1_0) == null)
        {
            arg_4B_0 = (Program.<>c.<>9__1_0 = new Func<string, bool>(Program.<>c.<>9.<Main>b__1_0));
        }
        Program.Where(arg_4B_0, "test");
    }

短い簡潔なラムダ式でしたら private static で外部に配置するのが最も良いコードに変換されるということです。

ただし、気を付けたいのはそもそもの「ラムダ式にしたい事情」というものは、例えばローカル変数へのスコープであったり他の様々な利点があってのことであるということです。

あと、delegate という単語は書きたくありません(ひっそりと本音)。

少し脱線しますが速いコードはGCが最も動かないコード・・・という原則ゆえかはわかりませんが、VS2015の診断ツールGCが動いたタイミングをマーキングしてくれて少しうれしくなります。 f:id:proprogrammer:20150722214123p:plain

最近チェックした記事

デリゲートのパフォーマンス

http://qiita.com/Temarin_PITA/items/d851e101cbce6dd92d86

久々にこれはビックリしました。
ラムダのあれは糖衣構文だと思い込んでいました。

久々にしてやられた感じです。
勉強させていただきました。

完成度の高い開発環境について

完成した、完成度の高い開発環境とはなんでしょうか。

諸説ありますが、個人的には以下の表現が可能なものだと思っています。

「XはXで自己定義可能」

例えば、C言語コンパイラC言語で制作可能です。D言語C#Javaも可能です。

プログラミング言語に限りません。自分自身の用途で自分自身を表現することができるかというところが重要だと感じます。

日本語は日本語自体で日本語の文法を定義することが可能です。

Microsoft ProjectはMicrosoft Projectの工程管理をすることができます。

UMLUML自体を定義・・・できましたっけ?

 

なんでこんな話になったかと言いますと、システムモデリングを行うとシステムを実際にビルドできるツールというものが台頭しつつあることについて、自分がそれらに極めて慎重であることの理由を考えたためです。(そういった試みは何十年も前からありますが。)

 

もう少し率直に申しますと、あるSE(主に設計の人)が「設計したシステムモデルからコードを出力することさえできればプログラマは必要ないんです。これからはそういう時代なんです。プログラマがコードを書くからバグを作り込むんです。アジャイルとかいってコードを書きまくっている人たちは、コードを書くのが好きだから書いているだけなんです。」と論じていたことについて、私自身はそれはどうでしょうねと思っていたためです。

 

アジャイルユニットテスト継続的インテグレーションを地盤として、既存のノウハウとの組み合わせによって正しい運用が可能ものとして考案されています。プログラマたちは別にリスクの高いコードを書きたいわけではなく、仕様書に沿ってコードを書くだけでは見つけられない問題点を補うための方法という位置づけだと解釈しています。

確かにバグを書くのはプログラマですが、プログラマがバグを直すので増やす一方ではありません。完成度の高い開発環境を用いて、正しい運用がなされているアジャイルは完成度の高いシステムを開発することができます。

(肝は、正しく運用されていれば完成された開発環境によって完成可能であることです。)

 

一方で、そのシステム記述ツールが正しくシステムを記述できるとして、その記述されたシステムも人間の記述であるためバグを内在しうることや、そのシステムが用いるコンポーネントや内部のコンパイラに完全性を期待できないことを忘れてはなりません。

プログラマが誤るように、設計者も誤るのです。

コンポーネントが、システム設計者の記述どおりに動かなかったときや必要なパフォーマンスを発揮できなかったときに、単にコンポーネントのせいにして仕事が完了するとも思えません。誰が直すのでしょう。

そのツール自身でそのツールを設計してそのツール自体をビルドできるなら、あるいは内部コンポーネントコンパイラ自体を定義できるならそれは素晴らしいと思います。

しかしそこまで細密な定義が可能になったツールはもはやコードと同レベルの複雑性と依存関係を持っているはずなのです。つまり、それはプログラマがコードを書くのと同じ話になります。

 一方それができないならその開発ツールは不完全なもので、人間や他の言語で書かれたコード群による補助を必要とします。

(肝は、正しく運用してもいかなる開発手法を用いても、ツールの不完全さによって「完成しないことがありうる」という事実です。)

  

極論はちょっと怖いと思いました。あれは彼のギャグだったというなら杞憂ですが。

 

まだまだ、プログラマの仕事がなくなることはあり得ないと感じますし、同じ意味でハードウェアのエンジニアの仕事がなくなることもあり得ません。設計者にもコンサルタントにも開発において持つべき分があります。

みんな、互助関係を保ちつつ自信を持って楽しく開発してもらいたいと思います。

WindowsFormsHostとGridSplitterのねばねばした関係

全ての画面を作るというわけにいかないプロジェクトもあるので、WindowsFormsHostをグリッド上に配置するというケースも時々あるかと思います。 そんなときに、GridSplitterを入れるとWindowsFormsのコントロールが酷い描画をするという話はよく聞きます。

そんなときの処方がこれです。

    public class ImmediateWindowsFormsHost : WindowsFormsHost
    {
        public ImmediateWindowsFormsHost()
        {
            SizeChanged += ImmediateWindowsFormsHost_SizeChanged;
        }

        private void ImmediateWindowsFormsHost_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            Child.Refresh();
        }
    }

あまり模範的なつくりではありませんが、要はリサイズしたときに即時再描画を行えばいいのです。 SizeChangedが最良かどうかもわかりませんが。

WindowsFormsHostの中身は別ウィンドウが単にWPFの画面上に乗っかっているにすぎません。 その性質を利用するということです。

ほんと・・・時として工数削減のための割り切りも必要になりますよね。

Msを16倍出し抜くC#+WPFの3回目(その前に)

スライドがどうも整ってないのでまだ公には出してないのですが。

今までくどくどと挙げてきたWPFボトルネックを全部避けてみるだけで、実際どの位速いパネルが生まれるのかというあたりをデモしました。

その時にちょっとおまけ話としてイベントの発射地点についてのまとめをしてみました。

MVVMが浸透した関係もあって、画面内のコントロールの使い方を誤る機会というものも多少減ったとは思うのです。
しかし、ScrollChangedとかSizeChangedとかのイベントの出所がどこのパスなのか、MicrosoftMSDNに正確に記載すべきじゃないかと思うのです。

上記2イベントはレイアウトパスでサイズ等々計算が終わったところで発射されます。
このイベントを受けてしまって、ついコントロールの描画にかかわる何かを書き換えてしまうと、バインディングパスやレイアウトパスはやり直しになる可能性があります。
いつになったら描画パスに移行できるのやら。

InvalidateVisualとかInvalidate系のメソッドの使用を諌めるよりもよほどこちらを気を付けるようにアナウンスしていただきたいと願ってやみません。
もちろん、上記メソッドもレイアウトパスをやり直させる効果があります。しかし、バインディングパスまでやり直しになる危険はありません。

本当に恐れるべきは、イベントの発射地点がどのパスにあるかということです。

そのあたりも、ぜひリファレンスコードを読んでみていただきたいと思います。

イベントの名前をここの検索ボックスで入力するだけです。
Reference Source

Thickness = 1の線分がどのくらい遅いか試すコード


WPF Benchmark - Source Code

私がここ数日言っていたことは、ここのコードでそのまま試せます。
走らせながら、太さを調整してみるとわかると思います。
モニターのデフォルト倍率によって多少変化しますが、要は実描画時に何ピクセルの太さになるかで速度が決まります。

あと、太さは顕著に影響しますが、ブラシの透過率は実は全然影響しないことも書き添えておきたいと思います。

高速な線分描画とか2

存分に強いグラフィックチップが有っても、太さ約1.7pxくらいが一番速いようです。グラフィックチップの種類はあまり問わず似たような性能曲線。
GPUの力もある程度働いているようです。でもCPU側が少々過剰でアンバランスです。

そろそろMicrosoftに問い合わせが必要な気がします。

高速な線分描画とか

あんまりショッキングな書き方をしたくありませんので、現時点ではある開発機で発生した事象として記載します。

始点終点がランダムである場合、線の太さが√3に近いほど高速になるという実測データを得ました。

大まかに言いますと 最速は 1.71px~1.74pxくらいの太さ範囲です。
太さが1pxの線を引く場合の6倍程度の速度でした(見過ごせますか?これ)。

無論、水平垂直の線分の話ではなく、自由に引いた線分のことではあります。
斜めの線を引くと、当然デフォルトでアンチエイリアスがかかるのですが、どうもその部分はハードウェアの恩恵が低いらしくある程度線が太くないと実は無駄にCPUパワーを浪費するということらしいですね。

実証コードは、当初業務で作成したため、持ち出すことはできておりません(当然ですお仕事ですから)。
ちょっと暇がありましたら再度作成してここに出してみようとは思います。が、誰か実証コード書いていただけるとありがたいなぁとは思っていたり思っていたりします。

チップによっても違うかなとは思っているのですが、しかしWPFアンチエイリアスは実はあんまりハードウェアの恩恵がありません(ご存知でしょうか)。なので、グラフィックチップによる差異は皆無あるいは僅差との推論をしております。

どなたか、計測結果を知らせていただければありがたいと思います。また、私の方でも及ぶ限りのベンチマークと実証コードを公開したいと思っております。

ちなみに、ある事実から予想は√2だったのですが、まんまとはずれてしまいました。関係者一同、√3かよ。という状態ですね。(笑)

さあ、これで自分のアプリで線分の描画を1.7くらいにできる人がいたら・・・・。いえ、何と言ったらよいやら。まあ、速度を堪能していただきたいです。活用法については、各自の技量としか。

delegate + ref が速いケース

foreachで12bytes程度以上の値型配列をループするより、delegateでref型を受ける形のループの方が速いのですね。

具体的な根拠は Measure It ですが。
Measure It - Home

明示的に delegate って書かないといけないところがちょっと苦しいですが。
ILコード的にはもっと短くて高速なコードが作れそうな気がします。