なんでもかんでも測りゃぁネタになるって思ってるうめでございますよ。今回もInvokeを測るですよ。
Invokeは別スレッドからおさわり自由
Windows フォームアプリでは「コントロールはメインのUIスレッドからだけ触っていいよ」というのが掟。破ればいやな感じの例外が起きて、店の裏に連れていかれて、怖いお兄さん方に………てなことはありませんが。blog なんかを検索すると Invoke すればいいんだよって書いてあるでしょう?だから、いろいろコードを書いてるときに何気なく Invoke 使うわけです。でもさ、Invoke って頻繁に呼び出すとすごく処理が遅い。
想像だけど、たぶんメインUIスレッドにメッセージ送ってるだけじゃねーのと思うわけ。で、spy++ するよね?すると、ThreadCallbackMessage ってやつを PostMessage してるらしい。別スレッドから Invoke するということはつまり、必要な処理をするメッセージを生成して、それをUIに送りつけているという感じなわけです。そりゃ早いわけないよな、ということでだいたいの処理時間を知りたいよね?そうすれば、処理時間の10倍くらいの間隔でないと Invoke はダメなんだなっていう適材適所が分かるし。
Invokeの時間を測る
そんなわけで、チェックボックスをチェックする、ラベルのテキストを書きなおす、テキストボックスのテキストを書きなおす、コンボボックスにアイテムを追加する、という3つのシナリオで時間を測ってみます。
コードの肝心なところは、こんな感じです。
#region Checkbox private delegate void CheckBoxDelegate(bool _check); private void CheckBoxChange(bool _check) { checkBox1.Checked = _check; } void testCheckboxThread(Object o) { Stopwatch swatch = new Stopwatch(); swatch.Reset(); swatch.Start(); CheckBoxDelegate dlg = new CheckBoxDelegate(CheckBoxChange); Invoke(dlg,true); swatch.Stop(); Debug.WriteLine("CheckBox," + swatch.ElapsedTicks + "," + swatch.ElapsedMilliseconds); } #endregion
これはチェックボックスの Thread を扱うところ。ラベルだとこんな感じ。以下省略。
private delegate void LabelDelegate(string s); private void labelText(string _s) { label1.Text= _s; } void testLabelThread(Object o) { Stopwatch swatch = new Stopwatch(); swatch.Reset(); swatch.Start(); LabelDelegate dlg = new LabelDelegate(labelText); Invoke(dlg, "Thread"); swatch.Stop(); Debug.WriteLine("Label," + swatch.ElapsedTicks + "," + swatch.ElapsedMilliseconds); }
で、スレッドを生成するところは、こんな感じ。
// スレッド動かす private void testThraed(Thread t) { t.Start(); } // テスト一巡 private void testStart() { checkBox1.Checked = false; label1.Text = ""; textBox1.Text = ""; Update(); testThraed(new Thread(testCheckboxThread)); testThraed(new Thread(testLabelThread)); testThraed(new Thread(testTextBoxThread)); testThraed(new Thread(testComboBoxThread)); }
このサンプルでは、CheckBox、Label、TextBox、ComboBox の順番ですが、実際のコードではテストの順番をランダムに入れ替えてます。
Invoke はだいたい10ミリ秒
タイマーで1000回テストを実行して、デバッグ出力をコピペして、Excelでデータ整理して、ミリ秒単位でヒストグラムを書きます。はいグラフ。
すごく時間がかかっているのは、他の処理が重なって忙しいときでしょうから、最頻値で比較しましょう。
- チェックボックスのチェック 8ミリ秒
- コンボボックス 6ミリ秒
- ラベルのテキスト書き換え 8ミリ秒
- テキストボックスのテキスト書き換え 6ミリ秒
というのがその結果。
いずれにしてもです。Invoke は手軽だけど、せっかく高速化する目的で作った別スレッド内で頻繁に Invoke 使っちゃうとダメってことですよ。ループの中に入れたりするのもご法度でしょうね。呼び出すにしても間隔は 0.1 秒くらいが限界。ループをぶん回して Invoke してたら全く処理が追いつかないし、別スレッドの意味はなくなりそうな感じです。肝に銘じておきましょう。
特に大事なことは、スレッドの中から Invoke を実行して制御が戻ってくるまでには時間がかかるってことです。70ミリ秒とか待たされることもある(上のグラフでは割愛してあります)。なので、タイムクリティカルな処理中のスレッドから Invoke を呼び出すなんてもってのほかってことですね。想像では、PostMessage したら終わりなんだから、すぐ制御が戻ると思っていたのですが、そうでもないらしいんですね。適材適所。
ある目的で調べたわけだけど、どうもこの手も使えんませんよということでした。Invoke 君さよならというわけで、お店からたたきだされました。
※ 今回の測定も、Intel® Core™2 Duo E7400 2.8GHz の搭載された Windows 7 Professional のマシンでおこないました。当然ですが、もう少し早いマシンではいい結果が、遅いマシンでは悪い結果が出ると思われます。また、同時に動作している別のア プリケーションにCPUタイムをとられてさらに遅くなる可能性もあります。実用上の性能は、ここで述べた性能よりもさらに低いかもしれないと考えておくべ きでしょう。
G+ のあるコミュニティで BeginInvoke を使えば非同期な呼び出しもできると教えてもらいました。改めてテストしてみようと思います。処理自体はけっきょくUIスレッドがおこなうわけなので、トータルで見た処理時間は変わらないのかもしれませんが、別スレッドのスループットは改善できそうな気がします。