マップエディタの機能拡張で、連続貼り付けに対応したところ、この処理が存外に重たかったので、その調査と改善を行いました。どこが重くて、どのように調査して、どう対応していったのか、実際の対応手順を元に追い込み方について説明したいと思います。


  • 最初の状態
少し長いですが、貼り付け処理のコードを全て掲載します。
最初のコード
処理内容はコメントにあるとおり。大きく5ブロックに分かれています。
  1. 編集モードを初期化する
  2. 現在の状態を保存する
  3. 貼り付ける
  4. アンドゥの最終処理
  5. 連続貼り付けモードなら再度初期化して移行する
まずはこのブロック単位で処理速度を計測するところから始めます。


  • 現在の処理の重さを計測する
上記のブロック単位でブレークポイントを張ります。
ブレークポイントを張った
これで実際に実行して、それぞれのブロック単位で実行速度を計測します。
最初の停止
貼り付けを行うと最初のブレークで停止します。続けて [F5] を押して処理を継続実行します。
処理時間-編集モードを初期化する
次のブレークで停止します。停止した行の右側に ≦ 20ミリ秒経過と表示されています。これが編集モードの初期化に要した時間となります。このように全てのブレークポイントを止めながら時間計測すると、以下のような結果となりました。念のため、2回計測しています。

20ms 20ms 編集モードを初期化する
78ms3ms 現在の状態を保存する
19ms7ms 貼り付ける
1ms1ms アンドゥの最終処理
7ms15ms 連続貼り付けモードなら再度初期化して移行する

2回目が異様に速くなってしまいました。これは CPUキャッシュに入ってしまったと考えられます。2回目の計測を捨てて、もう一度計測を貼り付けの状態を変更して行います。

20ms 37ms 編集モードを初期化する
78ms8ms 現在の状態を保存する
19ms17ms 貼り付ける
1ms1ms アンドゥの最終処理
7ms29ms 連続貼り付けモードなら再度初期化して移行する

これで、だいたいの傾向が見えてきたかと思います。この手の対応をする場合は、最初に一番重い場所を軽くする事を始めます。今回は、編集モードを初期化する、貼り付ける、連続貼り付けモードが特に重いですね。次点として現在の状態を保存するも確認してみます。
※ うちの突っ張り棚には必ずこれを使っています。2箇所でもう7年使っていますが、全くズレる気配がありません。安いしオススメです。

  • 編集モードを初期化する
ここは ResetEditMode(); を呼び出しているだけなので、この中身のどこでそんなに重いのかを確認します。まず普通に実行してこの行のブレークで止めます。
処理時間-編集モードを初期化する
この場所で [F11] を押すと、このメソッド内のステップトレースに移行します。これをステップインと呼びます。
編集モードリセット
ここで [F10] を押して 1行ずつトレースして各行の重さを確認していきます。すると、UpdateToolButton(); が重い事が分かりました。
編集モードを初期化する処理の重い部分
このメソッドはツールボタンの変化を反映させる処理です。連続して配置する場合はツールボタンの変化が無いはずです。そのため、この部分は連続配置の時は、共通メソッドの呼び出しを止めてしまえば軽くなるはずです。

単独配置は MouseDown イベントから呼び出されています。そして、連続配置は MouseMove イベントから呼び出されています。と言う事は、MouseMove からの呼び出しが連続配置と判断できますので、SetPaste メソッドの呼び出しに引数を付けて、単独配置か連続配置かを判断できるようにしてしまえば、UpdateToolButton(); の呼び出しを抑止できます。
連続配置か区別する引数を付与する
早速、SetPaste に引数を付けてみます。ブレークを張っていると、contSet が true で止めるのは難しいので、ここではメソッドが呼ばれた際に、ローカルから contSet を true に書き換えてテストしています。結果…
編集モードを初期化する処理が軽くなった
このように 二桁の処理時間が一桁まで高速化されました。


  • 貼り付ける
同様にどこが重いのかを [F10] ステップトレースで計測します。すると…
ハッシュ計算で3ms(1)
ハッシュ計算で3ms(2)
ハッシュ計算でのみ 3ms という処理速度が出ました。それ以外は 1ms です。たった 3ms と思うかもしれませんが、レイヤに対して最大5種類同時に貼り付けがあり得るのと、1ループで2回ハッシュ計算をしているので、3ms×2回×5レイヤ = 30ms もかかる事になります。これは重いですよね。

そこでなぜハッシュ計算をしているのか考えます。これは、貼り付けの結果、レイヤに変化があったかどうかを確認するために、ハッシュを取得しています。そして、ハッシュに変化があった場合は、アンドゥを有効としています。つまりアンドゥ処理の判定にハッシュ計算しているわけです。

Shift を押しながらの連続配置で、アンドゥで戻るのは一つずつじゃ無くてまとめて戻るようにしてしまえば、アンドゥ処理そのものが不要となります。連続配置は引数により既に判定可能となっています。そのため、連続配置ではアンドゥ処理を無くしてしまう事にします。高速化とは、このように状況把握と割り切りなのです。
連続配置ではアンドゥ処理を端折る
連続配置フラグを参照して、連続配置以外ならアンドゥ判定を行うようにしました。また、ついでに一度でも変化を検知したら、以降はハッシュ計算をスキップするようにして、通常時の処理速度の改善も期待できるようにしてみました。これで、処理速度をもう一度計測作してみます。
貼り付け処理が6msまで高速化した
20~30ms かかっていた処理が 6ms まで高速化されました。
※ 机の後ろの配線がごちゃごちゃするのを防止する見えない棚です。足下がぐちゃぐちゃだと思わずコンセント抜けたりとか危ないので、こちらの導入を検討してみてはどうでしょうか?

  • 連続貼り付けモード
ここまで、高速化の対応をしてきましたので、この連続貼り付けモードでの処理速度計測も分かりますね。そう [F11] によるステップインで、どの場所が重いか特定します。
初期化に編集/貼り付けモードのエントリを呼び出していた
すると、連続貼り付けモードの初期化に、メインメニューの編集/貼り付けエントリを呼び出していました。ここでは、編集モードの初期化、貼り付け画像の再作成と更新、貼り付け情報の初期化、編集モードを貼り付けに。チェックステータスを貼り付けに、ツールボタンの初期化という処理をまとめて行っている場所です。

連続貼り付けモードの場合は、既に貼り付けに関する初期化は完了しています。また、ツールボタンの変化もありません。つまり、ここで必要な処理は編集モードを貼り付けに戻す事だけだったのです。手抜きの結果、処理速度が遅くなっていたのですね…。さらに処理に関係なく、必ずツールボタンの更新まで律儀に呼び出していましたので、これは削除してしまいます。

編集/貼り付けエントリの呼び出しを止めて、編集モードを貼り付けに戻す1行を呼び出し元に記述します。これで計測します。 計測するまでも無く…
連続貼り付けモード再初期化高速化
はい、最速になったかと思います。


  • 最終動作確認
まずはどの程度高速化されたか、最終的な処理速度計測をしてみましょう。

37ms → 6ms 編集モードを初期化する
78ms → 1ms 現在の状態を保存する
19ms → 1ms 貼り付ける
1ms → 1ms アンドゥの最終処理
29ms → 1ms 連続貼り付けモードなら再度初期化して移行する

どうでしょうか。めっちゃくちゃ高速化が出来ましたよね。あとは実際に使ってみて、本当にバグっていないか確認して終了です。高速化を行う際の要点は以下の通りです。
  1. 状況の把握
  2. 修正方針検討
  3. 他に影響が出ないように実装
だいたい重いってのは、本来の処理に不要な余分な事をやっているモノです。そこを如何に把握するかで、その後の対策が決まると言って良いでしょう。そのため、VS2022 のブレークポイント、再実行、ステップトレース、ステップインなどのデバッグ機能を用いて、状況の把握を行う事が重要なのです。

この重い処理の箇所をボトルネックと呼んでいます。
※ ライトの場所がキーボード周辺だけに出来るので、余分な場所を明るくしなくてとても良い感じに手元だけ明るく出来ます。