Vim9 script はどれくらい速いのか
はじめに
Vim というテキストエディタは、vi から引き継いだ独特な操作を提供します。そしてそれだけでなく Vim script という独自のスクリプト言語で Vim 自身を拡張する事ができます。多くの Vim ユーザの方もそれはご存じかと思います。
もしかすると知らない方もいらっしゃるかもしれませんが、昨年末から Vim の作者 Bram Moolenaar 氏が vim9 というブランチを作り今年の1月に master ブランチへマージしました(vim9 とは言えどまだバージョンは8です)。その中に含まれていた機能が Vim9 script です。
そもそも Vim script
Vim script は Vim のバージョン 5.0 の頃から登場し、元々は設定ファイルである vimrc を環境により if 分岐する為に登場しました。その後 Vim script に色々な関数が足され、配列や辞書、浮動小数などもサポートされる様になりました。最近ではプロセス制御やネットワーク通信を行う為の job/channel も追加され、昔であれば外部コマンドに頼らないと実装できなかった多くの処理が Vim script だけで実装できる様になりました。
Vim script の問題点
Vim script には問題点がありました。1つは文法が曖昧過ぎる点。もう1点は遅いという点。
文法が曖昧すぎる問題
Vim script は Python や Perl から影響を受けていると言われています。Python のどこに影響を受けているかはモヤモヤしますが、Perl に関して言えば文字列結合を .
で行う所は似ていると言って良いでしょう。
function! s:test()
let a = 'Hello'
let b = ' World'
echo a . b
endfunction
call s:test()
この文法は初期の頃から使われていました。そしてその後、配列や辞書が追加されました。その際、辞書のアクセス方法に2つの方法が追加されてしまいました。
function! s:test()
let a = {'hello': 'world'}
echo a['hello']
echo a.hello
endfunction
call s:test()
どちらも同じ様に動作します。それは別に良いのですが、よく見て下さい。辞書のアクセスに .
を使っています。つまり前述の文字列結合とバッティングしているのです。
function! s:test()
let hello = ' mattn'
let a = s:get_string_or_dictionary()
echo a.hello
endfunction
call s:test()
さて、この a.hello
は
a
という変数に格納されている辞書のアイテムhello
を参照しているコード
でしょうか?それとも
a
という変数に格納されている文字列と、変数hello
を文字列結合しているコード
でしょうか?
答えは誰も分かりません。これを回避する為に多くの Vim プラグイン開発者は、[key]
による参照や .
の前後に空白を意図的に置くというハックを行ってきました。
function! s:test()
let hello = ' mattn'
let a = s:get_string()
echo a . hello
let a = s:get_dictionary()
echo a['hello']
echo a . hello
endfunction
call s:test()
とてもモヤモヤしますね。そしてこれが故に、Vim script はコンパイルする事ができなかったのです。なにせ AST (Abstruct Syntax Tree) に落とし込めないのですから。
※なお最近の Vim では ..
による文字列結合をサポートしています。どうしても不安だという方は a..b
の様にすると良いでしょう。(それでも前後に空白を入れるスタイルが筆者は好きです)
遅い問題
前述の通り、Vim script は文法が曖昧過ぎるため、コンパイルして高速化する事ができません。実はソースの解析も毎行実行しているのです。
function! s:test()
let a = [1, 2, 3, 4, 5]
for v in a
if v < 3
echo v
endif
endfor
endfunction
call s:test()
例えばこのコードを実行すると、Vim は...
for
を見つけv
を変数として、a
を配列としてパース、ループの戻り先としてマーク、if
を見つけa
の値を取り出しし3
と比較、分岐が真ならendif
までを処理、偽ならendif
までスキップ、if
の中身に入ってecho
をコマンドとして扱い変数v
の中身を表示、endif
とendfor
を見つけ、マークしたfor
に戻る
この様に、毎行パースと実行が繰り返されているのです。ですので for
文を実行すると割とコストの高い処理が実行されてしまいます。そりゃ遅くて当然なのです。
※もちろん遅くなる程の計算を Vim script でやるのか?という話にもなりますが
Vim9 script の登場
これらの問題を解決すべく登場したのが Vim9 script です。Vim9 script では前述の様な文法の曖昧さを無くし、変数の型縛りを導入し、さらにはコンパイルによる高速化が実現されています。
ただ Vim script が元々持っている物を引き継いでしまっている為、例えばウィンドウスコープを表す為の w:
等が変数の型指定 var w: number
とバッティングしている問題は未だ残っていますが。
初期実装時に Bram Moolenaar 氏が取ったベンチマークではJIT を有効にしていない Lua よりも速い結果が出たという話もあります。
以下がこれまでの Vim script
function! s:fib(n)
if a:n == 1
return 0
elseif a:n == 2
return 1
endif
return s:fib(a:n - 1) + s:fib(a:n - 2)
endfunction
function! s:benchmark()
let l:start = reltime()
echo s:fib(30)
echomsg str2float(reltimestr(reltime(l:start)))
endfunction
call s:benchmark()
以下が Vim9 script
vim9script
def Fib(n: number): number
if n == 1
return 0
elseif n == 2
return 1
endif
return Fib(n - 1) + Fib(n - 2)
enddef
def Benchmark()
var start = reltime()
echo Fib(30)
echomsg str2float(reltimestr(reltime(start)))
enddef
call Benchmark()
実行結果は以下の通り。
名称 | 処理時間 |
---|---|
Vim script | 20.287307 |
Vim9 script | 4.460179 |
この様に、Vim9 script は今までの Vim script よりもおおよそ5倍程度速くなっているのが分かります。
ならば本気を見せて貰おう
個人的にはプログラミング言語のパフォーマンス比較に Fibonatti 数列を使うのはフェアだとは思っていません。そこでより実用的な物を用意しました。
前者が Vim script で実装した、ロジスティック回帰によるアヤメの分類。後者が Vim9 script で実装した物です。
なお Vim script での実装の説明については以前 Qiita に書いた記事「Vim script でアヤメの品種を分類する」を参照して下さい。
学習率 0.01、エポック数 5000 で実行しました。
名称 | 処理時間 |
---|---|
Vim script | 51.258283 |
Vim9 script | 11.132494 |
この結果においても、Vim9 script はおおよそ Vim script の5倍程度速いという結果が得られました。
おわりに
Vim9 script は現在もなお絶賛開発中です。ちなみに先日 Vim9 script から let
構文が消えアタフタしました(var
に置き換えられた)。開発中の為、まだまだ速くなる可能性があります。今すぐ使いだすのはリスキーだとは思いますが、安定して動作する様になった際にはぜひ Vim9 script でプラグインを実装してみて下さい。
おまけ
ちなみにこれを Go 言語で実行するとどうなるか...
real 0.945
system 0.015
user 0.953
Go 言語はえー。
Discussion
vim9scriptがsyntax highlight表示される日が早く来ると良いですね。
Vim10が出た時点で陳腐化しそうな言語名はそうなる前に何とかしてほしい気がしますが