Vimで学ぶ「適切な速度」で処理をするということについて

この記事は atWare Advent Calendar 2015 の2日目の記事です。

昨年はボードゲームでリアルなコミュニケーションというタイトルでモノポリーについて記事を書きましたが、今年はモノポリーかVimのどちらの記事を書くか苦渋の末に迷ってVimをお題にしたので、イメージだけはモノポリーにしてみました。

今日書くエントリーは適切な速度で処理をするということをVimをとおして学んでみたいと思います。

Vimとは

一般的にVi互換で機能拡張が入ったエディタという事でプログラマやサーバー管理者などに普及しています。 弊社内の統計値によると72%の社員が今年になってこのエディタを使った事があるそうです。

このエディタの特徴として起動が速い事で一般的に知られています。もう少し突っ込んだ話しをするとVimというよりViが軽くてどこのサーバーにでもインストールされているということでしょうか。

さらにいうと、一般的なLinuxOSですとviコマンドがVimコマンドのvi compatibleモードのエイリアスとなっている事はわりと弊社社内をはじめVimを使った事ある人には知れ渡ってきていることですね。

さて、viコマンドの起動はなぜ速いのでしょうか?理由は結構わかりやすく、C言語で書かれてていることに加え、数十年速度や機能改善をされ続けた成熟したプログラムというのに加え、もう一つ大きな理由として余分な機能を読みこまず、起動しているという事です。

Vimの魅力としてカスタマイズできることにあります。VimはVim scriptというVim専用のスクリプト言語で機能拡張を行う事ができるのですが、デフォルトのVimではいくつかのデフォルトプラグインを読み込み起動しています。

ディストリビューションやカスタマイズビルド(例えばMacVimやKaoriyaパッチVimなど)版のVimによって違いはありますが、例えば

$ ls /usr/share/vim/vim73/plugin/
README.txt           netrwPlugin.vim      tohtml.vim
getscriptPlugin.vim  rrhelper.vim         vimballPlugin.vim
gzip.vim             spellfile.vim        zipPlugin.vim
matchparen.vim       tarPlugin.vim

私の環境のVimにはこんなプラグインがインストールされていました。 gzip.vimはvimでzipファイルを開いた時に展開せずにVimでアーカイブ内のファイルを閲覧・編集できるというプラグインです。viコマンドではこのcompatibleモードで起動するのでこのプラグインは読み込まれませんが、vimコマンドで起動すれば読み込まれてから起動します。

こういうデフォルトプラグイン数個なら体感的に差はありませんが、便利なプラグインがGitHubやvim.orgで公開されており、自分のスタイルに合った便利な機能を追加しようとプラグインをどんどん増やしていくと体感できるほどに遅くなる事があります。これは、Vimに限った事ではなく他のエディタでもよくあることですね。

ただ遅いだけではない

いよいよ本題に入っていきます。みなさんパフォーマンスチューニングの時に測定していますか? 今の時代なら感覚に頼らずメトリクスや速度測定をして数値に基づいてチューニングしていくのが一般的ですよね。

一般的な手法を用いて遅さを実感してみたいと思います。

VimはC言語で書かれてコンパイルされて実行されていますが、機能拡張は基本的にはVim sciprtという言語で動的に実されます。

まずは、Vimをプラグイン読み込みせず、compatibleモードかつvimの設定ファイルも読み込まずに起動速度を測定できるオプションもつけて起動してみましょう。

$vim -u NONE --noplugin --startuptime vim-startup.log

そうするとこんな結果になりました。

$cat vim-startup.log


times in msec
 clock   self+sourced   self:  sourced script
 clock   elapsed:              other lines

000.010  000.010: --- VIM STARTING ---
000.093  000.083: Allocated generic buffers
000.106  000.013: GUI prepared
000.494  000.388: locale set
000.500  000.006: clipboard setup
000.507  000.007: window checked
001.124  000.617: inits 1
001.137  000.013: parsing arguments
001.523  000.386: expanding arguments
005.239  003.716: shell init
005.492  000.253: Termcap init
005.535  000.043: inits 2
007.877  002.342: init highlight
007.880  000.003: sourcing vimrc file(s)
007.890  000.010: inits 3
007.910  000.020: setting raw mode
007.928  000.018: start termcap
007.950  000.022: clearing screen
008.044  000.094: opening buffers
008.047  000.003: BufEnter autocommands
008.051  000.004: editing files in windows
008.073  000.022: VimEnter autocommands
008.080  000.007: before starting main loop
008.681  000.601: first screen update
008.684  000.003: --- VIM STARTED ---

8msで起動できました。

VimScriptを読み込んで起動

こんな簡単な再帰呼び出しを行うシンプルなVim scriptを書きました。

$cat recursive.vim

"set maxfuncdepth=100
function! s:Recursive(count)
    "echo a:count
    if a:count > 1
        call s:Recursive(a:count - 1)
    endif
endfunction

call s:Recursive(1)

先ほどと同じ容量でVim scriptを読み込んで起動してみます。

$vim -S recursive.vim -u NONE --noplugin --startuptime vim-startup.log

結果を表示してみましょう。

$cat vim-startup.log

times in msec
 clock   self+sourced   self:  sourced script
 clock   elapsed:              other lines

000.006  000.006: --- VIM STARTING ---
000.077  000.071: Allocated generic buffers
000.088  000.011: GUI prepared
000.416  000.328: locale set
000.421  000.005: clipboard setup
000.427  000.006: window checked
001.019  000.592: inits 1
001.034  000.015: parsing arguments
001.421  000.387: expanding arguments
005.263  003.842: shell init
005.522  000.259: Termcap init
005.567  000.045: inits 2
007.934  002.367: init highlight
007.937  000.003: sourcing vimrc file(s)
007.947  000.010: inits 3
007.968  000.021: setting raw mode
007.985  000.017: start termcap
008.007  000.022: clearing screen
008.100  000.093: opening buffers
008.103  000.003: BufEnter autocommands
008.107  000.004: editing files in windows
008.332  000.080  000.080: sourcing recursive.vim
008.345  000.158: executing command arguments
008.346  000.001: VimEnter autocommands
008.351  000.005: before starting main loop
009.246  000.895: first screen update
009.248  000.002: --- VIM STARTED ---

なっ、なんと先ほど8msだった起動速度が9msになってしまいました。 1msも起動速度が遅くなってしまったらこれは非常に困ってしまいますね。 生産性が悪くなって仕方なくなるかもしれません。少し大げさでした...

Vim scriptの関数呼び出しを増やしてみる

先ほどは関数呼び出し1回だけだったものを100回の再帰呼び出しに変えてみます。 関数内では処理はないに等しいのでほぼ関数呼び出しだけのコストになります。

結果は下記の通り。

times in msec
 clock   self+sourced   self:  sourced script
 clock   elapsed:              other lines

000.008  000.008: --- VIM STARTING ---
000.090  000.082: Allocated generic buffers
000.103  000.013: GUI prepared
000.498  000.395: locale set
000.504  000.006: clipboard setup
000.511  000.007: window checked
001.193  000.682: inits 1
001.212  000.019: parsing arguments
001.740  000.528: expanding arguments
005.770  004.030: shell init
006.042  000.272: Termcap init
006.088  000.046: inits 2
008.466  002.378: init highlight
008.469  000.003: sourcing vimrc file(s)
008.479  000.010: inits 3
008.500  000.021: setting raw mode
008.518  000.018: start termcap
008.545  000.027: clearing screen
008.642  000.097: opening buffers
008.646  000.004: BufEnter autocommands
008.650  000.004: editing files in windows
010.080  001.279  001.279: sourcing recursive.vim
010.098  000.169: executing command arguments
010.100  000.002: VimEnter autocommands
010.105  000.005: before starting main loop
010.635  000.530: first screen update
010.638  000.003: --- VIM STARTED ---

先ほどは、9msで終わったところが10ms後半になってしまいました。 着目したいところがあるのでピックアップしてみたいと思います。

008.332  000.080  000.080: sourcing recursive.vim
010.080  001.279  001.279: sourcing recursive.vim

1回の関数呼び出しの際に80µsecだったところが、15倍ほど時間がかかっていますね。 関数呼び出し1つで数十µsecは遅いと感じましたか?速いと感じましたか?

あなたの普段使っている言語。例えばJavaやPythonやRubyやGoではどれくらいのコストでしょうか? 非機能要件でシステムのレスポンス要求のオーダーはどれくらいに設定していますか?

秒単位の事もあれば、msec単位のこともあります。これがレイテンシも含め数十msecで処理したいシステムなら致命的な程遅いのです。また、チリも積もればなんとやらでプラグインの用途によってはUIが体感的に遅いと感じてしまう事もあります。

そういう時にどういうアプローチがとれるでしょうか?

アプローチ

一般的なWebアプリと同じような発想のアプローチをとることができます。

Rubyでも速度を担保したい場合にnativeにコンパイルしたモジュールを部分的に利用することがありますね。同じようにVimからもネイティブなモジュールを呼び出す事ができます。 他にもVimではPythonやLuaのインターフェースを使うということもできますので、Vim script以外の言語を利用するというアプローチもありですね。

アプリケーションを丸ごと置き換えるのではなく部分最適というものです。

「適切な速度」で処理をするということについて

とは言ったものの、富豪的にリソースを扱える時代でもシビアにならないといけない時とそうでない時の差はやはり存在します。

Vimを例に出しましたが何ごとも適材適所で、実装でひたすらがんばるというのもやり方の一つですし、部分的なネイティブで部分的に最適化もいいでしょう。また、MessagePackやZeroMQのようものを使い、プロトコルを決めてロスを最小限にし効率良く処理するという、分割して粗結合にする方法もありかもしれません。

適切な速度を言語レベルで担保しようとするのか、システム全体で担保しようとするのか、それだけでも視点は違ってきます。

大きな視点で考えた場合には影響範囲は留まる所をしれませんね!! 非機能要件で必要な速度を担保しつつ、クラウドリソースを活用したうえでアーキテクチャ次第で色んなアプローチがとれるので、そこを考えていくのは本当に面白いです。

まとめ

本記事は@kamichiduさんの発表にインスパイアを受け書きました。 興味深い面白い発表してくださりそして発表後の小話にもつきあって頂きました。この場を借りて感謝の言葉をお伝えしたいです。ありがとうございます。

自分なりに普段の業務を通じて感じていた所がVimを通してあらためて気づけたというのは非常に面白かったです。なお、このエントリーはVim Advent Calendar 2015を兼ねません。

明日はYanouさんによる「IntelliJについて」です。というのを期待したいと思います!えっ!?