Vim 使いの「ブイ」(仮名)です。Vim のすゝめ改では、現代のテキストエディタについてのあらゆる話題をテーマに Vim の視点から見た話を行います。
今回のテーマは「なぜ Vim のシンタックスハイライトは遅いのか」です。
久しぶりに重い話を扱おうかと思っています。
1 Vim が遅い?
あなたは Vim でファイルを開いていて、「Vim が遅い」と感じたことはないでしょうか。
「Vim が遅い」にもいろいろな原因があり、プラグインが悪さをしていることもあります。しかし、外部のプラグインを入れていない状態でも遅いと感じたのなら、ほぼ「Vimのシンタックスハイライトが遅い」せいです。Vim のシンタックスハイライトを切れば症状は収まるはずです。
Vim のシンタックスハイライトを無効にするには、 syntax off
を実行すればよいです。
それではなぜ Vim のシンタックスハイライトは遅いのでしょうか。
実は Vim のシンタックスハイライトというのは正規表現のパターンの固まりとなっています。「シンタックスハイライトが遅い」というのは、「Vim の正規表現処理が遅い」に帰着できるわけです。
だとすると、Vim の正規表現を高速化する方法を考えなくてはいけません。どのようにすれば Vim の正規表現を高速化できるのでしょうか。
2 Vim の二つの正規表現エンジン
以下の issue を見てください。
https://github.com/vim/vim/issues/3937
以下のコマンドの実行が遅いと言われています。
$ time vim -i NONE -u NONE -c "s/^/0/" -c 'echo search("\\%((\\v0|!@!)+0")' -c 'qa!'
この問題の解決方法として上記の issue では set regexpengine = 1
という設定が提案されていますね。'regexpengine' というオプションは Vim の正規表現エンジンを切り替えるためのものです。
Vim には歴史的事情により「バックトラックエンジン」「NFA エンジン」の二つの正規表現エンジンがあります。
もともとは「バックトラックエンジン」のみでしたが、後から「NFA エンジン」が追加され、今では両方の正規表現エンジンによって正規表現は処理されています。
なぜ正規表現エンジンが二つとも追加されているかというと、それぞれのエンジンにより得意な正規表現と不得意な正規表現があるためです。どんなパターンでも高速な万能な正規表現エンジンは存在しないのです。
:help regexpengine
を見てみると、この値の初期値は 0 になっています。
*'regexpengine'* *'re'*
'regexpengine' 're' number (default 0)
global
This selects the default regexp engine. |two-engines|
The possible values are:
0 automatic selection
1 old engine
2 NFA engine
Note that when using the NFA engine and the pattern contains something
that is not supported the pattern will not match. This is only useful
for debugging the regexp engine.
Using automatic selection enables Vim to switch the engine, if the
default engine becomes too costly. E.g., when the NFA engine uses too
many states. This should prevent Vim from hanging on a combination of
a complex pattern with long text.
これは「バックトラックエンジン」「NFA エンジン」の両方を用いて処理します。「NFA エンジン」で処理できないパターンやパフォーマンスが遅いパターンを自動で判別して「バックトラックエンジン」に切り替える仕組みになっているので、本来は両方のエンジンの良いところどりができるはずなのですが、上記 issue を見れば分かる通り自動判別は完璧ではありません。
'regexpengine' の値を 1 に変更することで、正規表現は「バックトラックエンジン」のみを用いて処理するようになります。
3 Vim が不得意なパターン
よく問題となるのは Ruby のファイルです。Ruby は構文が複雑なので、それをシンタックスハイライトするために他と比較して複雑な正規表現が定義されています。これが非常に遅いようです。
他にも Vim が編集している行が極端に長い場合、正規表現のマッチが極端に遅くなるのでパフォーマンスの低下が発生します。これは正規表現でよく使われる .*
のような貪欲なマッチだと一度行の最後までマッチしてしまい、その後マッチしなくなるまでバックトラックを繰り返すため、行が長いとバックトラックが大量に発生し正規表現処理が重くなるのだと考えられます。
4 シンタックスハイライトのプロファイル機能
おそらく、ほとんどの Vim ユーザーが知らないと思いますが、Vim には遅いシンタックスハイライトを調査する :syntime
コマンドというものが存在します。これを用いることで、遅い正規表現パターンを調査することが可能です。
:help syntime
を参照すると使い方は単純明快です。
:syntime on
[ redraw the text at least once with CTRL-L ]
:syntime report
私の手元でも Ruby ファイルを編集した状態で syntime
を試してみました。
TOTAL COUNT MATCH SLOWEST AVERAGE NAME PATTERN
0.112071 1115 133 0.000797 0.000101 rubySymbol \%([{(,]\_s*\)\@<=\%(\h\|[^\x00-\x7F]\)\%(\w\|[^\x00-\x7F]\)*[?!=]\=::\@!
0.016030 1263 415 0.000095 0.000013 rubyConstant \%(\%(^\|[^.]\)\.\s*\)\@<!\<[[:upper:]]\%(\w\|[^\x00-\x7F]\)*\>\%(\s*(\)\@!
0.015054 1115 133 0.000069 0.000014 rubySymbol \%([{(,]\_s*\)\@<=[[:space:],{]\%(\h\|[^\x00-\x7F]\)\%(\w\|[^\x00-\x7F]\)*[?!=]\=::\@!
0.013281 983 0 0.000123 0.000014 rubyCapitalizedMethod \%(\%(^\|[^.]\)\.\s*\)\@<!\<\u\%(\w\|[^\x00-\x7F]\)*\>\%(\s*(\)\@=
0.013149 1115 133 0.000061 0.000012 rubySymbol []})\"':]\@1<!\<\%(\h\|[^\x00-\x7F]\)\%(\w\|[^\x00-\x7F]\)*[!?]\=:[[:space:],]\@=
0.012447 1322 396 0.000056 0.000009 rubyKeywordAsMethod \%(\%(\.\@1<!\.\)\|&\.\|::\)\_s*\%([_[:lower:]][_[:alnum:]]*\|\%(BEGIN\|END\)\>\)
0.011563 1366 384 0.000034 0.000008 rubySymbol [[:space:],{(]\%(\h\|[^\x00-\x7F]\)\%(\w\|[^\x00-\x7F]\)*[!?]\=:[[:space:],]\@=
0.009774 1031 48 0.000093 0.000009 rubyConditionalExpression \%(\%(^\|\.\.\.\=\|[{:,;([<>~\*/%&^|+=-]\|\<then\s\|\%(\<\%(\h\|[^\x00-\x7F]\)\%(\w\|[^\x00-\x7F]\)*\)\
0.009214 983 0 0.000053 0.000009 rubyUselessLineContinuation \%([.:,;{([<>~\*%&^|+=-]\|%(\%(\w\|[^\x00-\x7F]\)\@1<![?!]\)\s*\zs\\$
0.007968 983 0 0.000100 0.000008 rubyAttribute \%(\%(^\|;\)\s*\)\@<=attr\>\(\s*[.=]\)\@!
0.007331 956 0 0.000056 0.000008 neosnippetConcealExpandSnippets <`0\|<`\|<{\d\+:\=\%(#:\|TARGET:\?\)\?\|%\w\+(<|
0.007081 983 0 0.000045 0.000007 rubyPredefinedConstant \%(\%(^\|[^.]\)\.\s*\)\@<!\<\%(ARGF\|ARGV\|ENV\|DATA\|STDERR\|STDIN\|STDOUT\|TOPLEVEL_BINDING\)\>\%(\s*
0.006445 983 0 0.000044 0.000007 rubyCharacter \%(\w\|[^\x00-\x7F]\|[]})\"'/]\)\@1<!\%(?\%(\\M-\\C-\|\\C-\\M-\|\\M-\\c\|\\c\\M-\|\\c\|\\C-\|\\M-\)\=\%
0.006205 983 0 0.000030 0.000006 rubyRepeatExpression \%(\%(^\|\.\.\.\=\|[{:,;([<>~\*/%&^|+=-]\|\%(\<\%(\h\|[^\x00-\x7F]\)\%(\w\|[^\x00-\x7F]\)*\)\@<![!?]\)\
0.006200 983 0 0.000032 0.000006 rubyMultilineComment \%(\%(^\s*#.*\n\)\@<!\%(^\s*#.*\n\)\)\%(\(^\s*#.*\n\)\{1,}\)\@=
0.006119 1048 93 0.000042 0.000006 rubyKeywordAsMethod \<[_[:lower:]][_[:alnum:]]*[?!]
0.006100 983 0 0.000036 0.000006 rubyPredefinedConstant \%(\%(^\|[^.]\)\.\s*\)\@<!\<\%(FALSE\|NIL\|TRUE\)\>\%(\s*(\)\@!
0.006031 983 0 0.000056 0.000006 rubyPredefinedConstant \%(\%(^\|[^.]\)\.\s*\)\@<!\<\%(RUBY_\%(VERSION\|RELEASE_DATE\|PLATFORM\|PATCHLEVEL\|REVISION\|DESCRIPTI
0.005939 1160 195 0.000050 0.000005 rubySymbol []})\"':]\@1<!:\%(\h\|[^\x00-\x7F]\)\%(\w\|[^\x00-\x7F]\)*\%([?!=]>\@!\)\=
どうやら、Ruby のファイルの場合「シンボル」や「定数」を認識するための正規表現が複雑で遅いようです。個人的には遅い正規表現パターンに \@<=
や \@<!
\@=
といったものが使用されているのが気になります。\@
系のパターンは複雑な処理を要するので遅いです。使用を避けるべきだと思います。Ruby のシンタックスがいくら複雑とはいっても、このようなパターンを導入する必要はないのではないでしょうか。
Vim の正規表現の処理が遅いのはある程度仕方ないとして、シンタックスファイルの改善で対応できる部分もかなりあるのではないかと私は考えています。シンタックスファイルの改善を行うための材料は既に syntime
により揃っています。
5 Vim の改善状況
Vim 8.0.0647
https://github.com/vim/vim/commit/06f1ed2
'redrawtime' オプションがシンタックスハイライトに適用されるようになりました。これにより、redrawtime より遅いハイライトはスキップされます。
"slow regex search " 問題について
https://github.com/vim/vim/issues/3937
実をいうと、冒頭の遅い regex パターンがある問題は最新版の Vim ならばもう改善しています。Vim の正規表現エンジンはただ無策に遅いままではないということです。
$ time vim -i NONE -u NONE -c "s/^/0/" -c 'echo search("\\%((\\v0|!@!)+0")' -c 'qa!'
0.02s user 0.01s system 96% cpu 0.034 total
これは regexpengine=1
とほぼ同等の速度のようです。
$ time vim -i NONE -u NONE -c "set regexengine=1" -c "s/^/0/" -c 'echo search("\\%((\\v0|!@!)+0")' -c 'qa!'
0.02s user 0.01s system 95% cpu 0.029 total
6 neovim の改善状況
さて、neovim だとどうでしょうか。neovim と Vim はコアの部分のコードが同一なのでneovim にももちろん同じ問題があります。
https://github.com/neovim/neovim/issues/650
https://github.com/neovim/neovim/issues/650#issuecomment-359299343
Note: 7/10 日現在、最新の neovim でも直っていませんでした。neovim 特有の問題として neovim は Vim のパッチを全て取り込めていないので、Vim に取り込まれている正規表現パフォーマンスの改善の取り込みが遅いからでしょう。
A common performance problem that was seen when the new regex engine was
introduced was lookbehind assertions. Those were enhanced to allow specifying a
maximum number of bytes to look behind instead of being unbounded.
neovim のメンテナによると、Vim/neovim の NFA 正規表現が遅いときは、ほとんどの場合バックトラックが必要な複雑な正規表現を処理しているとの見解があるようです。
This article is made by Vim.