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

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

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コード的にはもっと短くて高速なコードが作れそうな気がします。

仮想化しないでも速いCanvasの作り方

XAML Advent Calendar 2014 - Qiita

こちら向けの記事です。1日遅刻しました。ごめんなさい。

あちこちで書いて回っておりますがPanel.Childrenがとても重いのです。 遅い原因はこちら。 http://referencesource.microsoft.com/#PresentationFramework/Framework/System/Windows/Controls/Panel.cs,1001

同じ階層のVisualはVisualTreeに結合した順番でしか表示できないので、ChildrenやPanel.ZIndexが変更される都度、Panel.ZIndexを使ってこのRecomputeZStateが走ります。 レイアウトパスの直前に1回だけしてくれたらいいのにと思うかもしれませんが、残念ながらそのような作りにはなっていないのが現状です。

とすると、FrameworkElementに独自のChildrenを実装してやるしか手がない。ということになります。 幸いなことに、XAMLで記述する場合も [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] という属性をつけて、IList<UIElement>を実装してやればいいので、結構簡単なんです。

<Window x:Class="FastCanvas.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:FastCanvas"
        Title="MainWindow">
    <ScrollViewer HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible">
        <local:Canvas x:Name="_canvas" Width="3200" Height="32000"></local:Canvas>
    </ScrollViewer>
</Window>

今日の主役は仮想化しないで高速なCanvasなので、XAMLはこのくらいです。 XAMLの話の予定でしたのにすみません。 一応、100000個くらいのシェイプをゾロッと配置してみます。

using System.Windows;
using System.Windows.Media;
using System.Windows.Shapes;

namespace 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 rectangle = new Rectangle() { Width = 32, Height = 32, Fill = Brushes.Aquamarine, RadiusY = 12, RadiusX = 12 };
                    FastCanvas.Canvas.SetLocation(rectangle, new Point(x * 32, y * 32));
                    _canvas.Children.Add(rectangle);
                }
            }
        }
    }
}

そして、これが問題のCanvas

using System.Windows;
using System.Windows.Media;
using System.ComponentModel;
using System.Collections.ObjectModel;

namespace FastCanvas
{
    public class Canvas : FrameworkElement
    {
        public static Point GetLocation(DependencyObject obj)
        {
            return (Point)obj.GetValue(LocationProperty);
        }

        public static void SetLocation(DependencyObject obj, Point value)
        {
            obj.SetValue(LocationProperty, value);
        }
        public static readonly DependencyProperty LocationProperty =
            DependencyProperty.RegisterAttached("Location", typeof(Point), typeof(Canvas),
            new FrameworkPropertyMetadata(default(Point),FrameworkPropertyMetadataOptions.AffectsArrange));

        [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
        public ObservableCollection<UIElement> Children{get; private set;}

        public Canvas()
        {
            Children = new ObservableCollection<UIElement>();

            Children.CollectionChanged += Children_CollectionChanged;
        }

        void Children_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            if(e.OldItems != null)
            {
                foreach (UIElement oldItem in e.OldItems)
                {
                    RemoveVisualChild(oldItem);
                }
            }
            if(e.NewItems != null)
            {
                foreach (UIElement newItem in e.NewItems)
                {
                    AddVisualChild(newItem);
                }
            }
        }

        protected override int VisualChildrenCount
        {
            get
            {
                return Children.Count;
            }
        }

        protected override Visual GetVisualChild(int index)
        {
            return Children[index];
        }

        protected override Size ArrangeOverride(Size finalSize)
        {
            foreach(var child in Children)
            {
                var location = GetLocation(child);

                child.Arrange(new Rect(location, child.DesiredSize));
            }

            return base.ArrangeOverride(finalSize);
        }

        protected override Size MeasureOverride(Size availableSize)
        {
            foreach (var child in Children)
            {
                var fe = child as FrameworkElement;

                if( fe != null )
                {
                    child.Measure(new Size(fe.Width,fe.Height));
                }
            }

            return base.MeasureOverride(availableSize);
        }
    }
}

100000のシェイプを配置しても、Panel.Children.Addにあるようなもっさり感はありません。 ZIndexが不要ならこの程度で十分行けてしまうという程度で考えていただけると良いかなと思います。