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について」です。というのを期待したいと思います!えっ!?