Vim9 script はどれくらい速いのか

公開:2020/10/14
更新:2020/10/14
5 min読了の目安(約4500字TECH技術記事

はじめに

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 の中身を表示、endifendfor を見つけ、マークした 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 数列を使うのはフェアだとは思っていません。そこでより実用的な物を用意しました。

https://github.com/mattn/vimscript-logistic-regression-iris

https://github.com/mattn/vim9script-logistic-regression-iris

前者が 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 言語で実行するとどうなるか...

https://github.com/mattn/go-gonum-logisticregression-iris

real    0.945
system  0.015
user    0.953

Go 言語はえー。