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