「立て!立つんだビムー!」

この記事は Vim アドベントカレンダー 2012 の 19 日目の記事です. 昨日はhrsh7th さんの vim-versions についての記事 でした.

たくさんプラグインを入れたり設定を書いたりしていると Vim の立ち上がりはどんどん遅くなってしまいます. 一度 Vim を立ち上げたらそのあと閉じることが無いスタイルの人はそれほど気にならないかもしれませんが, シェルからターミナル内で Vim を開いたり閉じたりするスタイルの人にとっては起動速度はとても大事です.

今年のアドベントカレンダーでは,素敵なプラグインを入れて Vim の機能を強化する記事がたくさん紹介されているので, ここではそういった便利さをなるべく維持しつつ,起動時間を抑える方法を紹介します.

(1/3 追記) neobundle.vim がバージョン 3 になり,ファイルタイプ・コマンド・関数名・マッピングのいずれかで読み込みタイミングを指定する機能が追加されました.

アウトライン

  • 要らないプラグインを取り除く
  • 要らない設定を取り除く
  • autoload 以下の関数をなるべく使わないようにする
  • プラグインの読み込み自体を遅延する(neobundle.vim 編)
    • NeoBundleLazy の autoload オプションを使う (1/3 に追記)
    • 特定の filetype でのみ読み込む
    • gVim のみで読み込む
    • 特定のコマンドを使った時に初めて読み込む
  • プラグイン化して autoload に移す
  • augroup を1つに統一する
  • 時間のかかる処理をなるべく遅延する
  • 読み込む vimrc を制限する
  • vimrc 読書会に参加する
  • まとめ

要らないプラグインを取り除く

色々プラグインを試していると,「これ良さそうだなー」と思って入れてみて結局あまり使わなかったプラグインなどがどうしても出てきてしまいます. そういったプラグインを見つけて取り除くことで,起動時間を改善できます.

どのプラグインの読み込みにどの程度の時間がかかっているかは --startuptime オプションを付けて起動することで 確認できます.

vim --startuptime hoge

として Vim を起動すると,Vim が起動時のファイルごとの読み込み時間がカレントディレクトリの hoge というファイル に書き込まれます. 第1カラムに起動開始からの時間(ミリ秒),第2カラムにファイルの読み込みにかかった時間(ミリ秒)が出ているはずです. あとはそれを見て,読み込み時間がかかっていて,かつほぼ使っていないプラグインを削っていきます.

要らない設定を取り除く

次に vimrc のどの部分にどれぐらい読み込み時間がかかっているかを調べます. 上記の --startuptime オプションではファイルごとの読み込み時間しか分からないため,.vimrc の各行の読み込みにどれくらい かかっているかを知るためには mattn さんの書いた benchvimrc または +profile 付きでビルドされた Vim を使います.

.vimrc の各行と,それぞれの行の読み込みにかかった時間が分かるので,それを元にあまり使っていなくて,かつ読み込みに時間の かかっている部分を削っていきます.

ここまでで,まずは「あまり有用ではないのに読み込みに時間をかけてしまう」部分を効率的に見つけて削る方法を示しました. 次からは,プラグインや設定の機能を損なわずになるべく起動時の読み込み時間を抑える方法について,もう少し細かい部分 を見ていきます.

autoload 以下の関数をなるべく使わないようにする

Vim の関数で名前に # が入っているものは,各プラグインの autoload ディレクトリ内に定義されている関数です. 例えば,unite#do_action()autoload/unite.vim ファイルの中に定義されている関数になります. autoload 以下のファイルは実際に関数が呼ばれ,ロードする必要が生じたときに初めてロードされます. 詳しくは :help autoload を参照してみて下さい.

.vimrc 内においても autoload 以下で定義された関数をなるべく呼び出さないようにすることで,余計な autoload 以下のファイル の読み込みを減らし,.vimrc の読み込み速度を上げることができます.

実際に VimShell での例を見てみます. VimShell では,拡張子とコマンドを紐付けておくことができます.

let g:vimshell_execute_file_list = {}
call vimshell#set_execute_file('txt,vim,c,h,cpp,d,xml,java', 'vim')

とすると,例えば ./hoge.c と実行するだけで vim コマンドが実行されてそのファイルを開けます. しかし,この設定は vimshell#set_execute_file() を呼び出しているため,この時点で autoload/vimshell.vim の読み込みが必要になります. この関数がやっているのは,各拡張子をキーに持ちそれに対応するコマンドを値に持つ辞書を変数 g:vimshell_execute_file_list に定義している だけです.よって,次のように書きかえることができます.

let g:vimshell_execute_file_list =
               \ { 'txt' : 'vim', 'vim' : 'vim', 'c' : 'vim', 'h' : 'vim',
               \   'cpp' : 'vim', 'xml' : 'vim', 'java' : 'vim' }

これだと 'vim' の繰り返しが多すぎて煩わしいという人は for 文を使うともう少し綺麗に書けます.

let g:vimshell_execute_file_list = {}
for ext in split('txt,vim,c,h,cpp,d,xml,java', ',')
    let g:vimshell_execute_file_list[ext] = 'vim'
endfor

vimshell#set_execute_file() を呼び出さないようにすることで,Vim 起動時に autoload/vimshell.vim をロードせずに済みます.

プラグインの読み込み自体を遅延する(neobundle.vim 編)

プラグインによっては,特定の状況下でのみ力を発揮するものがあります.例えば clang_complete という C++ のコードを静的に 解析して補完候補を生成するプラグインは,当然ながら C++ のコーディングをしているときしか使いません. そういったプラグインを常にロードする必要はありませんし,起動時にロードする分は無駄になってしまいます.

そこで,プラグインマネージャの力を借りて,本当にそのプラグインが必要になるまで読み込みを遅延します. 僕はプラグイン管理に neobundle.vim を使っているので,neobundle.vim を 使った場合について書きます.pathogen や vundle,unbundle などを使った場合できるのかどうかは各プラグインのヘルプを参照して下さい.

NeoBundleLazy の autoload オプションを使う (1/3 に追記)

neobundle.vim ver 3 から NeoBundleLazy コマンドに autoload オプションが追加されました. これにより,プラグインの読み込みタイミングを - 特定のファイルタイプが読み込まれた時 - 特定のコマンドを使用した時 - 特定の関数が呼ばれた時 - 特定のマッピングが実行された時 のいずれかまで遅延することが出来るようになりました. 以下は neobundle.vim の help に書いてある例です.

" ファイルタイプが c か cpp のファイルを読み込む時に clang_complete をロードする
NeoBundleLazy 'Rip-Rip/clang_complete', {
        \ 'autoload' : {
        \     'filetypes' : ['c', 'cpp'],
        \    },
        \ }

" TweetVimHomeTimeline コマンドを使う時に TweetVim をロードする
NeoBundleLazy 'basyura/TweetVim', { 'depends' :
        \ ['basyura/twibill.vim', 'tyru/open-browser.vim'],
        \ 'autoload' : { 'commands' : 'TweetVimHomeTimeline' }}

" <Plug>(smartword-w),<Plug>(smartword-b),<Plug>(smartword-ge) のいずれかのマッピングを実行する時に vim-smartword を読み込む
NeoBundleLazy 'kana/vim-smartword', { 'autoload' : {
        \ 'mappings' : [
        \   '<Plug>(smartword-w)', '<Plug>(smartword-b)', '<Plug>(smartword-ge)']
        \ }}

" vcs#info() 関数か Vcs コマンドを実行しようとしたときに vim-vcs を読み込む
NeoBundleLazy 'Shougo/vim-vcs', {
        \ 'depends' : 'thinca/vim-openbuf',
        \ 'autoload' : {'functions' : 'vcs#info', 'commands' : 'Vcs'},
        \   }

詳細については :help neobundle-options-autoload を参照してみて下さい.

特定の filetype でのみ読み込む

neobundle.vim バージョン 3 から,autoload オプションで指定できるようになったため,このセクションは不要になりました

先ほどの clang_complete のような例です.NeoBundleLazy でプラグイン登録だけを行なっておき,FileType イベントで実際に 対象のファイルタイプのファイルが読み込まれた時に初めてプラグイン本体を読み込むようにします.

僕が実際に設定している C++ の例です.

" プラグインとして登録されるだけでまだ読み込まれない

" C++11 対応シンタックスファイル
NeoBundleLazy 'vim-jp/cpp-vim'
" clang を使った静的コード解析
NeoBundleLazy 'Rip-Rip/clang_complete'
" ISO 直近のドラフト N3337 を閲覧するための unite ソース
NeoBundleLazy 'rhysd/unite-n3337'

" ファイルタイプが cpp なファイル(= C++ ソースコード)が読み込まれたときにプラグインを読み込む
augroup NeoBundleLazyLoadCpp
    autocmd!
    autocmd FileType cpp NeoBundleSource
                \ cpp-vim
                \ clang_complete
                \ unite-n3337
augroup END

gVim のみで読み込む

errormarker.vim のように GUI でしか機能しないプラグインや カラースキームの読み込みは CUIVim では無意味です. 下記の例では,GUI でしか使わないカラースキームを gVim 起動時のみ読みこむようにしています.

" .vimrc に記述
NeoBundleLazy 'ujihisa/unite-colorscheme'
NeoBundleLazy 'tomasr/molokai'
NeoBundleLazy 'altercation/vim-colors-solarized'
NeoBundleLazy 'earendel'
NeoBundleLazy 'rdark'
NeoBundleLazy 'telamon/vim-color-github'

" .gvimrc に記述
NeoBundleSource unite-colorscheme
                \ molokai
                \ vim-colors-solarized
                \ earendel
                \ rdark
                \ vim-color-github

特定のコマンドを使った時に初めて読み込む

neobundle.vim バージョン 3 から,autoload オプションで指定できるようになったため,このセクションは不要になりました

特定のファイルタイプに依存しない,gVim と 端末内の Vim の両方で使うプラグインであっても, まだ読み込みを遅延する方法があります.

例えば,VimFiler はとても便利で GUICUI を問わずでも使用しますが,ファイル操作をしたいときにしか使いませんし, 読み込みに結構時間がかかるのが気になります. そういった場合は,コマンドを関数でラップしてしまい,最初にコマンドが呼ばれた時にプラグインをロードする処理を追加 する方法があります. これは daisuzu さんの vimrc で実際に実装されています.

https://github.com/daisuzu/dotvim/blob/master/.vimrc#L2212

VimFiler のコマンドを LoadVimFiler() という関数でラップし,LoadVimFiler() 内では初回呼び出し時に VimFiler 本体をロードする処理を行なっています.

プラグイン化して autoload に移す

プラグイン化するほどでもない機能を vimrc 内に実装することはよくありますが,それに色々付け足していくことでだんだん 実装が大きくなり読み込みに時間がかかってくるようになることがあります. その部分を抜き出し,新しいプラグインの autoload に移してしまうことで,その部分の読み込みを抑えることができます. clever-f.vim は最初は vimrc 内で f マッピングを拡張するために書かれたものでしたが,実装が大きくなってきた & f を実際に使うまでは読み込みは 不要なのでプラグインとして切り出し,関数を autoload に移しました.

augroup を1つに統一する

「要らない設定を取り除く」の章で説明した方法で実際に vimrc の読込状況を眺めた方はお気づきかもしれないですが, autocmd! は地味に時間のかかるコマンドです. よって,余分な augroup を作らず,1つのグループにまとめることで少し改善することができます.

例えば,次のような設定があった場合,

augroup SettingCpp
    autocmd!
    autocmd FileType cpp inoremap <buffer>;; ::
augroup END

augroup SettingRuby
    autocmd!
    autocmd FileType ruby inoremap <buffer><C-s> self.
augroup END

augroup SettingHaskell
    autocmd FileType haskell nnoremap <buffer><silent><Leader>ht 
                  \ :<C-u>call <SID>ShowTypeHaskell(expand('<cword>'))<CR>
    function! s:ShowTypeHaskell(word)
        echo join(split(system("ghc -isrc " . expand('%') . " -e ':t " . a:word . "'")))
    endfunction
augroup END

という,それぞれのファイルタイプごとに augroup を作るのでは無く,

" 最初に vimrc 全体で使う augroup を定義しておく
augroup MyVimrc
    autocmd!
augroup END

" ...

" FileTypeDetect グループに追加したい autocmd イベントを記録
autocmd MyVimrc FileType cpp inoremap <buffer>;; ::
autocmd MyVimrc FileType ruby inoremap <buffer><C-s> self.
autocmd MyVimrc FileType haskell nnoremap <buffer><silent><Leader>ht 
              \ :<C-u>call <SID>ShowTypeHaskell(expand('<cword>'))<CR>
function! s:ShowTypeHaskell(word)
    echo join(split(system("ghc -isrc " . expand('%') . " -e ':t " . a:word . "'")))
endfunction

とすることにより,autocmd! の回数を減らすことができました.

ただし,autocmd! で後にクリアすることを前提に考えられている augroup (例えば後で出てくる UniteCustomActions) ではイベントをクリアするためにグループを切らなければならないのと,augroup を特定の autocmd コマンド群のネームスペースの ように使っている場合は可読性がやや低下する場合があるため,導入する際はそのあたりを考慮すべきです.

時間のかかる処理をなるべく遅延する

あまり広く使える方法では無いですが,時間のかかる処理を特定の autocmd イベントが発生するまで遅延 する方法があります.考え方としては,NeoBundleLazy のときと同じです.

unite#custom_action() の実行を遅延する例を挙げます. unite#custom_action() は特定の種類の候補に対して自作のアクションを追加する関数です. Mac の Finder でディレクトリを開くアクションはこんな感じで .vimrc 内に書けます.

" Finder action for Mac 
if has('mac')
    let finder = { 'description' : 'open with Finder.app' }
    function! finder.func(candidate)
        if a:candidate.kind ==# 'directory'
            call system('open -a Finder '.a:candidate.action__path)
        endif
    endfunction
    call unite#custom_action('directory', 'finder', finder)
endif

しかし,unite アクションは実際に unite や vimfiler を立ち上げるまで必要になりませんし, unite#custom_action() の実行にも結構時間がかかります. そこで,FileType イベントを利用して,実際に unite.vim や vimfiler が読み込まれるまで 上記のコードが実行されないようにします.

" 遅延したい処理を関数として定義しておく
function! s:define_unite_actions()
    " Finder for Mac 
    if has('mac')
        let finder = { 'description' : 'open with Finder.app' }
        function! finder.func(candidate)
            if a:candidate.kind ==# 'directory'
                call system('open -a Finder '.a:candidate.action__path)
            endif
        endfunction
        call unite#custom_action('directory', 'finder', finder)
    endif

    " 一度読み込むともうこの関数は呼ばなくて良いのでイベントをクリアしておく
    autocmd! UniteCustomActions
endfunction


" filetype が unite か vimfiler のときにカスタムアクションを初めて定義する
augroup UniteCustomActions
    autocmd!
    autocmd FileType unite,vimfiler call <SID>define_unite_actions()
augroup END

読み込む vimrc を制限する

「要らないプラグインを取り除く」の章で紹介した方法で Vim 起動時のファイルの読み込み状況を眺めた方はお気づきかもしれないですが, Vim はデフォルトでシステムの vimrc とユーザの vimrc の両方を読み込みます. もしシステムのほうの vimrc を読みこまなくても良ければ,次のようなコマンドで Vim を起動することで,読み込む vimrc をユーザのもの のみに制限できます.

vim -u $HOME/.vimrc

ただし,システムの vimrc の中にも有用な設定があるので,本当に読み込まなくて良いのか,ちゃんと中身を確かめておくべきです.

vimrc 読書会に参加する

lingr の Vim 部屋 にて,毎週土曜日の 23:00 から vimrc 読書会 が開催されています. 毎週誰かの vimrc を題材にしてあーだこーだ言う会です.他の人の vimrc はとても参考になりますし, Vim に詳しい方々も多く参加されているので,色々質問する機会でもあります.

まとめ

今回の記事は自分が実際に行った改善を元に書いたので,どれくらい参考になるかは分かりませんが,少しでも Vim の起動速度改善に貢献できたなら幸いです. 普段 vimrc の見直しを特にしないという方も,vimrc の掃除をしてみてはいかがでしょうか.年末ですし.

参考までに上記の改善を行なうことによって,Vim の起動時間は下記のように変化しました.

  • before: 454 ms → after: 364 ms

明日のアドベントカレンダー担当は @chikatoike さんです.

蛇足

本当は help のキーワード指定のコツなどの help を使いこなす tips を書こうと思っていたのですが,下記の場所に十分まとまった情報があったのでやめました. 参考までに.