sakura-editor / sakura

SAKURA Editor (Japanese text editor for MS Windows)
https://sakura-editor.github.io/
Other
1.25k stars 163 forks source link

スクロール操作時の処理負荷について #445

Open beru opened 6 years ago

beru commented 6 years ago

長いドキュメントを開いて縦スクロールバーをマウスで上下に動かす操作を良く行うので(主に目grepの為)その操作を行う時にどこの処理に時間が掛かっているかを Visual Studio 付属の Performance Profiler で調べてみました。 (「そんな非効率なオペレーションを頻繁に行うな」という心の中のツッコミは抑えて下さい)

以下がプロファイル結果のスクリーンショットです。

image

CEditView::OnPaint

46.91 % で半分程度占めていますが、テキストエディタでたくさん文字を描画するのでこれはまぁなんとなく理解出来ます。根本的にはここを改善しないといけないのですが、気になったのはそれ以外の場所です。

CEditView::ScrollDraw

24.85 % 掛かっていますが、デバイス互換BMPを WindowsAPIの BitBlt でスクロール処理するところが原因です。

この呼び出しは省略しても見た目には問題が起きないに思えます。良く分からないので質問しますが、動作環境や設定、操作内容によってはそうでもないのでしょうか?どなたか教えて下さい。

共通設定全般画面キャッシュを使う のチェックボックスでデバイス互換BMPを使うかどうかは設定で変えられるので使わなければこの問題は回避可能ですが、それ以前にデバイス互換BMPを使う事のメリットを自分が分かっていません。どなたか(ry

CEditView::AdjustScrollBars

13.78 % でそこそこ割合が多いのが気になって調べてみたところ、WindowsAPIの SetScrollInfo 関数を 第4引数の redraw を TRUE にして呼び出ししていますが、何故かこの値が TRUE だと処理に時間が掛かるようです。

FALSE に変えると全然処理時間が掛からなくなります。FALSE に変えても特に見た目に変化は無く支障は無いように思えます。環境とか設定とか操作手順によっては問題が起きてしまうのでしょうか?どなたか分かる方がいたら教えて下さい。

_DispEOL 関数

8.00 % 掛かっていますが、行末記号を表示するだけで 8 % も食っているのが不思議なので調べてみたところ、WindowsAPI の CreateRectRgnIndirect 関数等に時間が掛かっていたりして、他にも描画色用のペンを CreatePen 関数で描く度に作成したり、PolyPolyline 関数で描画するのにも時間が掛かっていました。

これに関しては、DIBを作って自前で描画してしまうのが改善策だと思います。

まとめ

サクラエディタの描画周りで気になる事として、本来は各種操作イベント時にデバイス互換BMP(DIB)に描画しておき、WM_PAINT メッセージのタイミングでは保持しているデバイス互換BMP から BitBlt するだけにしておけば重い描画を繰り返す必要は無い筈なのですがそうなっていない事です。歴史的経緯等の色々な理由はあると思いますが。。

スクロールバーについてもOSのウィンドウを使わずに、自前で描画した方が表示タイミングを同期出来る等、色々とメリットがあると思います。

描画周りの改造は言葉で書き連ねるのは簡単ですが実際に実装するのは結構大変そうです。複雑に入り組んでいるというかコード量が多いというか…。

berryzplus commented 6 years ago

CEditView::ScrollDraw 24.85 % 掛かっていますが、デバイス互換BMPを WindowsAPIの BitBlt でスクロール処理するところが原因です。

サクラエディタの描画機構は全般的に効率が悪いです。ぼくは実装の経緯を知らんのですが、アプリ全体のコードを見た感想としては、その対策として作られたと思われるのが互換ビットマップによるキャッシュだったのだろうと思っています。

なんとなく、おそらくご存じだと思うんですが、BitBlt関数は GDI 関数の中で最も高速に動作する命令の1つです。時間がかかっているのは転送元矩形と転送先矩形の位置決めなのではないかという予想。

CEditView::AdjustScrollBars 13.78 % でそこそこ割合が多いのが気になって調べてみたところ、WindowsAPIの SetScrollInfo 関数を 第4引数の redraw を TRUE にして呼び出ししていますが、何故かこの値が TRUE だと処理に時間が掛かるようです。

SetScrollInfo はスクロールバーの thmb の位置を windows に教える目的で呼び出している認識です。ちょっと記憶があいまいなんですが、描画順序が整理されてなくて一度の描画処理の中で何度も呼ばれるような作りになっていた気がします。最後の呼出しで bRedraw = TRUE にするのは必須な認識です。速度改善の観点で話をすると、SendMessage で 処理結果を待つ必要はないはずだから PostMessage に切り替える作戦が有効かも知れません。

_DispEOL 関数 8.00 % 掛かっていますが、行末記号を表示するだけで 8 % も食っているのが不思議なので調べてみたところ、WindowsAPI の CreateRectRgnIndirect 関数等に時間が掛かっていたりして、他にも描画色用のペンを CreatePen 関数で描く度に作成したり、PolyPolyline 関数で描画するのにも時間が掛かっていました。

これは完全に同意です。いつかそのうち提案したいと思っていて、代替案をモノクロDIBにするかカラーDIBにするかWMFにするかで迷っていた感じです。DIBでいくならDIBで。設定変更されたタイミングでDIBを作りなおす機構にしとけば高速化できそうな気がします。

まとめ

統一感がないのがカオスを作りだしている元凶だとぼくは思っています。 共通クラスとか共通関数とか、作りだすとめんどくさいんですけどねぇ・・・。

全部一気にやるのは無理だと思うので、こちらは一旦レイアウト構築周りから攻めていこうかな、と考えているところです。

beru commented 6 years ago

サクラエディタの描画機構は全般的に効率が悪いです。ぼくは実装の経緯を知らんのですが、アプリ全体のコードを見た感想としては、その対策として作られたと思われるのが互換ビットマップによるキャッシュだったのだろうと思っています。

デバイス互換ビットマップのキャッシュってあんまり有効に使われている雰囲気が無いんですよね。ベンチマークとか用意しないと断言出来ませんが。。

なんとなく、おそらくご存じだと思うんですが、BitBlt関数は GDI 関数の中で最も高速に動作する命令の1つです。時間がかかっているのは転送元矩形と転送先矩形の位置決めなのではないかという予想。

転送元矩形と転送先矩形の位置決め自体はスカラー変数を使った演算(座標計算)が主体だと想像しているので、そんなにCPUの命令数は数百万も必要ないし大量のメモリ転送も起きないので処理時間は掛からない筈だと思っています。

BitBlt (Bit Block Transfer) の処理速度自体には手を入れられないので、

等が気になります。

beru commented 6 years ago

SetScrollInfo はスクロールバーの thmb の位置を windows に教える目的で呼び出している認識です。ちょっと記憶があいまいなんですが、描画順序が整理されてなくて一度の描画処理の中で何度も呼ばれるような作りになっていた気がします。

仮に描画処理の中で何度も呼ばれるとしても、1回の呼び出しが十分軽ければ問題にならないと思います。気になるのは何故引数の redraw が TRUE だと重くて FALSE だと軽いのか、です。

最後の呼出しで bRedraw = TRUE にするのは必須な認識です。

これは描画をさせる為に必要、という事でしょうか?

自分の環境だと FALSE にしてもスクロールバーの描画は行えているように見えます。

速度改善の観点で話をすると、SendMessage で 処理結果を待つ必要はないはずだから PostMessage に切り替える作戦が有効かも知れません。

これは、SetScrollInfo の実装が SBM_SETSCROLLINFOSendMessage で送っているので、自前で PostMessage を使うようにするという事でしょうか?自前で送信するのは推奨されないようですが、それで速くなるならそれも有りかもしれないですね。

どこのコードがきっかけてどのウィンドウメッセージが送られているのかを簡単に調べられたら良いですね。

例えば、メニューの 設定ミニマップを表示 で画面右端にミニマップが表示されるようにして、画面右端をつまんでリサイズ操作をすると、横幅を縮める操作時に描画が走って、開いているドキュメントの内容によっては全体を描画する処理負荷が大きくて、フレームレートが低下します。

追ってみたところ、CEditWnd::OnSize2 メソッドで、::MoveWindow( m_pcEditViewMiniMap->GetHwnd(), ~~ が有ると WM_PAINT が呼び出されていて、リサイズ時の本来は描画が必要ないと思われるケースでも毎回文書の描画をしているので重くなっている事が分かりました。

beru commented 6 years ago

これは完全に同意です。いつかそのうち提案したいと思っていて、代替案をモノクロDIBにするかカラーDIBにするかWMFにするかで迷っていた感じです。DIBでいくならDIBで。設定変更されたタイミングでDIBを作りなおす機構にしとけば高速化できそうな気がします。

WMFというのはWPF(Windows Presentation Foundation)の事ですか?何の略でしょうか?

何やらDirectWriteで描画した方が表示品質を上げられるようなので、それにも対応してみたいです。表示速度をどこまで上げられるのかは分かりませんが。。

時代遅れなGDIはいつか捨てた方が良いのかもしれませんが、昔に比べてCPUも十分速いのでハードウェアアクセラレーションが効かない DIB (Device Independent Bitmap) を使っても、描画処理を無駄が無いように組めば十分快適に表示できるとは思います。DDB (Device Dependent Bitmap) を使った場合の GDI のハードウェアアクセラレーションが今の時代にどこまで有効なのか知りませんし、WDDM (Windows Display Driver Model) についても分かっていませんが…。

beru commented 6 years ago

統一感がないのがカオスを作りだしている元凶だとぼくは思っています。 共通クラスとか共通関数とか、作りだすとめんどくさいんですけどねぇ・・・。

共通化するとフレームワーク的になっていくので設計のセンスとか経験もいるでしょうね。Qt の誘惑が聞こえますがバイナリサイズが重かったりで抵抗感が…。

全部一気にやるのは無理だと思うので、こちらは一旦レイアウト構築周りから攻めていこうかな、と考えているところです。

コードの規模が大きいので全部一気にやるのは無理ですね。先人が積み上げてきたコードのお陰で快適なテキストエディタになっているわけなので有難味も感じますが…。

beru commented 6 years ago

WMF は Windows Metafile の略みたいですね。しかしこのファイルフォーマットは古すぎるしGDI任せになるので微妙過ぎるかなと思います…。

berryzplus commented 6 years ago

wmf・・・そうです、古代技術ですw emf、拡張メタファイルとも呼ばれている気がします。

gdiの描画命令をひとまとめに保存しておくことができて、 ビットマップと同様、スタンプのようにペタッと描画することができます。 もちろん、ビットマップ同様に動的生成できます。 高DPI対応を考えたときにどっちがいいか判断に迷っていて選択肢に入れていました。 DPIスケーリングを考えて画像を生成するならDIBで問題ないと思います。

画像をペタッと貼る戦略は、Direct3D(=Direct Draw)でも使えるので今後そっちに移行しても改行マークを画像化する作業は無駄にならないんじゃないかな、と思ってます。 https://msdn.microsoft.com/ja-jp/library/windows/desktop/ff934857.aspx

berryzplus commented 6 years ago

何やらDirectWriteで描画した方が表示品質を上げられるようなので、それにも対応してみたいです。表示速度をどこまで上げられるのかは分かりませんが。。

テキスト描画、高精細ディスプレイに限定した話ですけど、そうらしいです。 https://docs.microsoft.com/en-us/windows/desktop/DirectWrite/introducing-directwrite

クリアタイプフォントで使えるサブピクセルレンダリングの話がキレイに見える理由とか。

さっきサラッとぐぐってみて気付いたんですが、EmEditorがDirectWrite対応したみたいですね・・・。 描画速度改善で必要になるなら、そちらのサポートに回ってもよいです。

個人的には内部処理系のキケロサポートを早めになんとかしたいです。 https://qiita.com/teatime77/items/05f00934956a0f42dd27

サクラエディタは IMM32.dll に依存するアプリなので、 標準インストールの英語版Windowsでは動作できません。

まずは日本国内向けのユーザビリティ向上でしょ?と言われればそれはそうだと思ってるので、そっち優先でいって大丈夫です。(といいつつ、計画中は内部処理改善がメインとか言う・・・)

beru commented 6 years ago

テキスト描画、高精細ディスプレイに限定した話ですけど、そうらしいです。 https://docs.microsoft.com/en-us/windows/desktop/DirectWrite/introducing-directwrite

クリアタイプフォントで使えるサブピクセルレンダリングの話がキレイに見える理由とか。

http://download.microsoft.com/download/6/1/4/6143C6AF-9173-41F3-8981-761AE28E85FD/PDC08_Introducing_DirectWrite_JPN.docx

この資料で結構細かく解説されてました(、、って紹介してくれたURLでも同じような内容で解説されていますね)。登場してからもう10年が経ってるんですね。文明はどんどん発達していく…。

個人的には内部処理系のキケロサポートを早めになんとかしたいです。 https://qiita.com/teatime77/items/05f00934956a0f42dd27

キケロサポートって何でしょうか?ぐぐっても見つかりません。

berryzplus commented 6 years ago

キケロはヨーロッパの詩人の名前で、TextServicesFrameworkの開発コードネームです。

beru commented 6 years ago

おー、そうなんですね。説明ありがとうございます。それに対応する事の実利が良く分からないです。。

それはともかく、IME周りの呼び出しもちょっとCPU食ってたりします。

今回貼り付けたプロファイル結果だと、 CEditView::SetIMECompFormPos の中で ImmSetCompositionWindow の呼び出しで 2.38 % くらいです。 ImmGetContext した後に、ImmGetOpenStatus の呼び出しを追加して、戻り値が TRUE の時だけ呼ぶようにすればその分を削れます。

berryzplus commented 6 years ago

それに対応する事の実利が良く分からないです。。

ま、それは追々・・・導入提案のときにもっかい初めから書くことになるので、またそんときにw

それはともかく、IME周りの呼び出しもちょっとCPU食ってたりします。

これは変換候補リストを出す位置をIMEに知らせてる処理だと思っています。知らせるべきタイミングは、一番最初(初期表示)とキャレット移動時とスクロール時です。別な観点から ImmSetCompositionWindow の呼出回数を減らす方法はないものかと思ってます。たとえば前回通知したキャレット座標を保存しておく、とか。

キャレット: テキストエディタの入力位置を示す、点滅するアレのこと カーソル: マウスカーソルのこと。入力位置を示すモノを指す用語なのでキャレットをカーソルと呼んでも正しいっぽい。

込み入った話になってくると技術系専門用語が・・・w

beru commented 6 years ago

これは変換候補リストを出す位置をIMEに知らせてる処理だと思っています。知らせるべきタイミングは、一番最初(初期表示)とキャレット移動時とスクロール時です。別な観点から ImmSetCompositionWindow の呼出回数を減らす方法はないものかと思ってます。たとえば前回通知したキャレット座標を保存しておく、とか。

変換候補リストを表示していないのにスクロール中に位置を頻繁に設定する必要があるかは疑問です。 実際に ImmGetOpenStatus で確認して FALSE が返った場合には ImmSetCompositionWindow を呼び出さないようにしても、変換候補リストが後で変な位置に表示されるという事は特に無いようです。

beru commented 6 years ago

https://github.com/sakura-editor/sakura/issues/445#issuecomment-421561526 で自分が書いた下記のコメントですが、

デバイス互換ビットマップのキャッシュってあんまり有効に使われている雰囲気が無いんですよね。ベンチマークとか用意しないと断言出来ませんが。。

共通設定全般画面キャッシュを使う のチェックボックスを外すと矩形選択時に表示のちらつきが起きる事が最近分かりました。という事で有効でした。

without_screen_cache

k-takata commented 6 years ago

VimでもDirectWrite対応によって、カラー絵文字が表示できるようになったりしましたが、うまくやらないとかえって遅くなったりすることもあります。

beru commented 6 years ago

gvim_8.1.0468_x64-mui2.exe をインスコして大量のテキストを貼り付けてスクロール試してみましたが表示がちらつきます。もしかしたら表示が速すぎてモニタのリフレッシュレート以上に描画してるとか…。240Hz 対応のゲーミングモニタ買わないと駄目ですね。

k-takata commented 6 years ago

デフォルトだとDirectWriteはoffですので。.vimrc

set encoding=utf-8
set guifont=MS_Gothic:h11
set renderoptions=type:directx

などと書いておかないと有効になりません。 GDIモードだとそもそもダブルバッファリングはしていない上に勝手なタイミングで描画しているのでちらつきます。 DirectWriteは、システム側でバッファを持っていて、リフレッシュレートに同期して切り替えてくれているようなので、ちらつきは(ほとんど?)出ないですね。

beru commented 6 years ago

おぉ、本当だ。全然ちらつかなくなりました。ありがとうございます。 サクラエディタもこれぐらい速いと良いなぁ…。

beru commented 5 years ago

改行記号の描画について調べてみました。

改行記号の描画にCPU使用率が思ったより食われる現象は、PolyPolyline で描画する改行記号を32bitDIBにキャッシュして使いまわしてもあまり減りませんでした。しかし画面キャッシュをDC互換ビットマップから32bitDIBに変更する対策も併せて入れる事で(改行コード描画のCPU使用率が)結構減る事が確認出来ました。描画元と描画先のフォーマットを揃えたら速くなるのかもしれません。もしかしたらDIBでなくてDDBでも良いのかも??そこらへんは未確認です。

改行記号は32bitのBitmapにキャッシュしないでも 1bit の monochrome bitmap でも良いかもしれないですが、GDIをうまく使う事が出来ませんでした…。

改行記号の背景色と下線の描画は、_DispEOL の中の ExtTextOutW_AnyBuild の呼び出しで行われているようです。 表示フォントサイズを変えても改行記号の線のピクセル幅は変わりませんが、下線は太くなっていきます…。まぁこの不統一は改行記号に下線表示するニーズも無いと思うので気にしないで良いと思います。

BitBlt より TransparentBlt の方がCPU使用率が高くなりますが、BitBlt だと下線を表示した場合に後の描画で背景色で上書きしてしまう問題があります。下線を表示しない場合は軽い BitBlt を使うようにするのもありかもしれません。下線を表示しない場合は背景色の描画を ExtTextOutW_AnyBuild で行う必要も無いので呼び出しを削ってCPU使用率を下げられそうです。

確認に使用した変更内容はなんだか突っ込みどころが多いので PR を出す気になれません。:dizzy_face: 改行記号の描画のCPU使用率が減ったとしても全体の表示速度が速くなったかの確認は別途必要ですし。。

berryzplus commented 5 years ago

ちょこちょこ実験してるんですけど、FillRect vs PatBltだとPatBltのが速いって話があります。 http://tete009.seesaa.net/article/19578707.html

sakura-editorのコードは基本FillRectで、ところによりExtTextOutで代用(何故だ!)だと思います。 PatBltに変えることで減らせるコストがありそうなんで、どうやってPRにまとめるかを思案中です。

なんとなく効率的が気になる感じの複雑な混色計算がところどころに存在しているんですけど、

beru commented 5 years ago

メモリ使用量は増えてしまいますが、「画面キャッシュを使う」の設定項目を削除して必ず内部で 32bit DIB (BGRA) を作ってそこに描画するようにするのが良いと思います。指定領域の塗りつぶしもメモリを書き換える事で出来るので、GDIの関数経由よりそちらの方が速いと思います。

beru commented 5 years ago

ちょこちょこ実験してるんですけど、FillRect vs PatBltだとPatBltのが速いって話があります。 http://tete009.seesaa.net/article/19578707.html

sakura-editorのコードは基本FillRectで、ところによりExtTextOutで代用(何故だ!)だと思います。 PatBltに変えることで減らせるコストがありそうなんで、どうやってPRにまとめるかを思案中です。

なんとなく効率的が気になる感じの複雑な混色計算がところどころに存在しているんですけど、

ExtTextOut で代用しているのは紹介して頂いたページで書かれている CDC::FillSolidRect もどきのやり方だと思います。単色矩形塗りつぶしは良く使うので HBRUSH を指定する FillRect とは異なるまた別のGDI関数を Microsoft が大昔に追加してくれていたら良かったんですが…。

PatBlt の方が速いというのは動作環境によって違うかもしれないし今でもそうなのかは不明ですね。紹介してくれたページでソースコードとかも公開されていたら良かったですね。