cafegale(LeafCage備忘録)

LeafCage備忘録(はてなダイアリー)と統一しました。

Vim scriptの実行時間を計測する7つの方法

この記事はVim Advent Calendar 2012 : ATND 245日目の記事です。

Vim scriptは実行速度が遅いことで有名ですが、どこでどれくらい時間がかかっているのかを調べたい時があります。今回はVim scriptの実行時間を計測する方法をまとめました。

起動時間を調べる

1. --startuptimeオプションを付けて起動する

$gvim --startuptime {ファイル名}

Vim起動時に読み込んだスクリプトと、かかった時間をリストアップしたファイルを、ホームディレクトリに生成します。
起動時しか計測できないのと、結果がファイルに出力されるので(工夫をしない限りは)気軽に見られないということと、ファイルの見方がイマイチ分からないのが難点です(後述の:profileコマンドで出力されるのと同じ形式です)。
ファイルに追記していく方式なので、--startuptimeで起動する度にログファイルが大きくなっていきます。

あるスクリプトの実行時間を調べる

2. :profileコマンドを使う

前述の--startuptimeを任意のスクリプトや関数に適用するイメージです。ただし、Vimが"huge"バージョンでないと利用できません。

:echo has('profile')

で利用できるか確認できます。Shougoさんの働きかけで最近、Kaoriyaバージョンでも利用できるようになったらしいです。
ただし、profileが利用できるバージョンは全てのスクリプトの実行速度が遅くなるそうです。
スクリプトファイル全体の実行時間を計測することも、関数を計測することも出来ます。
それより細かい単位の計測は出来ません。
はじめに`:profile start {filename}`をしないと、`profile func {funcpattern}`や`profile file {filepattern}`を実行できません。
また、Vimを終了しないと結果は書き出されません。

関数名「lily#*」に一致する関数のそれぞれの実行時間を計測したいとき
:profile start vimprofile.txt
:profile func lily#*
ファイル名「lily.vim」のスクリプト実行速度を計測したいとき
:profile start vimprofile.txt
:profile file */lily.vim

Vim終了時に計測結果がvimprofile.txtに書き出されます。

3. benchvimrc-vimを使う

こちらは"huge"バージョンでなくても利用可能ですが、vimrc限定です。vimrcを複写してから各行にreltime()を埋め込んで計測する方式なので、再読込可能なvimrcでないといけません。

モジュールの実行時間を調べる

4. vim-benchmarkを使う

ある書き方と別の書き方、どちらの方が速いかを調べることが出来るオブジェクトを生成します。
オブジェクトに作られた関数の実行速度を比較する形式です。

5. TimerStart、TimerEndコマンドを使う

計測を始めたいところにTimerStart、終えたいところにTimerEndと書けば2点間の処理時間を計測できます。
vimrcに、ほんの2行ほど付け足せばいいだけので導入が簡単です。

command! -bar TimerStart let start_time = reltime()
command! -bar TimerEnd   echo reltimestr(reltime(start_time)) | unlet start_time

それでいて任意の2点間を調べることも、ベンチマークを取ることも出来るので応用性があります。
実行するとすぐに結果が表示されるので自動で呼び出されるような処理の計測には不向きです。
また、計測対象のスクリプトにこのコマンドを埋め込まなくてはいけません。

6. laptime.vimを使う

laptime.vimはスクリプト2点間とその中間点を計測できます。

(laptime.vimの使い方)
function! s:test()
  let g:lt = laptime#new() "計測を始める場所でlaptime#new()する
  let num = 2
  let i = 1
  call g:lt.lap()          " .lap()を呼ぶと、測定開始からの時間を記録
  while i < 5
    let num = num * i
    let i += 1
    call g:lt.lap('ループ中')  "各関数に引数を渡すことが出来る
  endwhile
  call g:lt.end()          "計測を終える場所で .end()を呼ぶ
  return num
endfunction

echo s:test()
call g:lt.show()           " .show()を呼ぶと、結果を表示する
48

       TOTAL       LAP
  1:   0.000058    0.000058
  2:   0.000159    0.000101  > ループ中
  3:   0.000262    0.000103  > ループ中
  4:   0.000361    0.000099  > ループ中
  5:   0.000459    0.000098  > ループ中
  6:   0.000578    0.000119

この場合実行に合計0.000578秒かかったことになります(計測時のオーバーヘッド含む)。
そしてLAPというのが前の計測点から次の計測点までにかかった時間です。
文字列以外も引数として渡すことが出来ます。変数の変動を見たいときに使えます。複数の引数を渡すことも出来ます。その場合表示は複数行になります。

function! s:test()
  let g:lt = laptime#new('変数の変化も見る')  "タイトルをつけることも出来ます。
  let num = 2
  let i = 1
  call g:lt.lap()
  while i < 5
    let num = num * i
    let i += 1
    call g:lt.lap(i, num)             "複数引数を与えることが出来ます。
  endwhile
  call g:lt.end(num)
  return num
endfunction

echo s:test()
call g:lt.show()
48
変数の変化も見る
       TOTAL       LAP
  1:   0.000058    0.000058
  2:   0.000160    0.000102  > 2
                             2
  3:   0.000265    0.000105  > 3
                             4
  4:   0.000362    0.000097  > 4
                             12
  5:   0.000460    0.000097  > 5
                             48
  6:   0.000579    0.000120  > 48

さらに、laptime.vimで計測した過去の結果は、g:laptimesに蓄えられています*1
これをやめさせたければ、g:laptimes.disableに0以外の値を代入してください。
g:laptimes.show()で過去の計測結果を見ることが出来ます。

:call g:laptimes.show()
[1]
       TOTAL       LAP
  1:   0.000058    0.000058
  2:   0.000159    0.000101  > ループ中
  3:   0.000262    0.000103  > ループ中
  4:   0.000361    0.000099  > ループ中
  5:   0.000459    0.000098  > ループ中
  6:   0.000578    0.000119

[2]
変数の変化も見る
       TOTAL       LAP
  1:   0.000058    0.000058
  2:   0.000160    0.000102  > 2
                             2
  3:   0.000265    0.000105  > 3
                             4
  4:   0.000362    0.000097  > 4
                             12
  5:   0.000460    0.000097  > 5
                             48
  6:   0.000579    0.000120  > 48

これによって、、同じスクリプトに手を加えて変更前と比較することもできます。また、趣旨から外れますが、プラグイン作成における発見しにくいバグである、意図しない再帰的な自分の呼び出しを見つけることが出来ます(laptime.vimの計測が終わらない*2うちに同じ場所で新しくnew()されると再帰呼び出ししているとみなし、g:laptimes.show()の結果にCONTAIN CONTAINED標記が付きます)。

注意しなければいけないのは、緻密な計測には向いていません。私の環境ですと、.lap()の実行に約0.0001秒のオーバーヘッドがありました。

7. reltime() reltimestr()を直接使う

let s:start = reltime()
  "{...計測したい処理...}
echo reltimestr(reltime(s:start))

まとめ

お手軽な測定だと、TimerStart TimerEndコマンド
プラグイン製作におけるデバッグを兼ねた測定だと、laptime.vim
精密さが求められる測定だと、 :profileコマンド
Vim起動時間の測定だと、--startuptime

を使うのがいいでしょう。

*1:この例ではlaptime#new()をグローバル変数g:ltに収めていますが、いつでもg:laptimesで確認できるので、ローカル変数を使っても構わないのです

*2:.end()にまで達していない

第56回vimrc読書会

http://lingr.com/room/vim/archives/2013/07/27#message-16017813

mattn
undolevel を一時的に 0 にすれば undo がまとまるというtips
あんまり知られてないよね > undolevel=0

http://lingr.com/room/vim/archives/2013/07/27#message-16018089

全角スペースハイライト問題

deris0126
L79:pluginでmatch使われてると効かなくて悲しみに包まれる
https://github.com/bitc/vim-bad-whitespace
でmatch使われていてずっと効かなかった…

cohama
>でmatch使われていてずっと効かなかった…
matchadd() が正解ですね

cohama
>matchadd()での正しいやり方いまいちよくわかってないのでsyntaxでやってしまっている。
あーそういえば、そういう方法もあるのか。matchadd はウィンドウローカルなので微妙に困ることがあるけど、Syntax はどうなんだろ

LeafCage
matchadd()使っても複数ウィンドウ開いていると1つのウィンドウでしかハイライトされないと思いますし。
ハイライトでハイライトした方がいいと思います。

thinca
syntax でやると色々面倒な問題が起きるので私はmatchadd()使ってるなぁ

http://lingr.com/room/vim/archives/2013/07/27#message-16017905

GitHubにpushする手順

今まで新しいリポジトリ作る度に親切なGitHubが次にやる手順を載せてくれていたから、特に覚えておく必要ないかーとか思ってたけど、forkした時などにはそんなのを載せてくれないので、やっぱり覚え書いとく。

リポジトリ作成時に表示される内容(SSHの場合:標準)

touch README.md
git init
git add README.md
git commit -m "first commit"
git remote add origin git@github.com:LeafCage/temp.git
git push -u origin master

リポジトリ作成時に表示される内容(HTTPの場合)

touch README.md
git init
git add README.md
git commit -m "first commit"
git remote add origin https://github.com/LeafCage/temp.git
git push -u origin master

forkした後、あるいはcloneした後

なぜかgit@github.com:LeafCage/temp.gitへのpushが弾かれる。

git push https://github.com/LeafCage/temp.git

なら通る(ただしユーザ名とパスワードを毎回求められる)

こんなの見つけました。

こうすればいいらしい。

git remote set-url origin git@github.com:LeafCage/temp.git
git push

グローバル変数などでディレクトリを指定する時には末尾に「/」を付けない

Vim script plugin作成時において、ディレクトリをユーザに指定させる変数を用意する時に、

let g:foo_dir = '~/foo/bar/'

これは良くない。
なぜなら、mkdir(expand(g:foo_dir), 'p')をするとエラーになるからである。
mkdir()に通すなら、実行時に末尾の「/」を除去するか、または初めから

let g:foo_dir = '~/foo/bar'

のように、末尾の「/」抜きで定義されているべきである。

ほとんどのプラグインは後者であるので、それに合わせるべきである。
(私は今まで末尾に「/」付けないとそれがファイルなのかディレクトリなのか分からないだろ常考という考えでプラグインディレクトリ定義変数は末尾に「/」を付けていたが、改めることにする)

ただし、fnamemodify(dir, ':p')等で使われる「':p'」は、ディレクトリの場合は末尾に「/」を付けてくるので、対象がディレクトリであることを前提に、「/」なし流儀に合わせるなら、fnamemodify(dir, ':p:h')を使うべき。

uptodate.vimの原本ファイルを更新したら自動でruntimepathの通っているパスにある同名ファイルも更新されるようにした

LeafCage/uptodate.vim
原本を編集してから→それを他の場所にコピー、という手間が馬鹿馬鹿しく感じられたので、
書き込み時に自動で原本を複写するようにした。

ただし、もしかしたら誤判定で間違った場所にコピーされるかも知れない。
そういった不具合を見つけた時には報告してください。

なお、この修正に伴い、前回の記事uptodate.vimでもっとお手軽にオレオレライブラリ - cafegaleを一部書き換えた。

さらに、autoload/uptodate.vim自身にも、複数の中から最新版を読み込ませるギミックを持たせた

自分を読み込ませる前に自分を呼ぶなんてことは出来ないので、スクリプト先頭に専用コードを直書きする形にした。

uptodate.vimでもっとお手軽にオレオレライブラリ

この記事は Vim Advent Calendar 2012 223日目の記事になります。
222日目はもぷりさんのもぷろぐ: あなたの Vim は もっと Smart に Input できるでした。
224日目は@BOXPさんのVimでClojureする時のあれこれ - はこのLINUXです。

Vim scriptを作り続けていると再利用したい部分、共通化したい部分が出てきます。
本日は自分専用のライブラリを作るのに役立つ新たなプラグインを紹介します。

ライブラリとして単独のプラグインでリリースする手もありますが、そうするとプラグイン間で依存関係が出来、作者、利用者とも管理が面倒くさくなります。
そこで個々のプラグインにライブラリを組み込むことが出来る、vital.vimたるものが開発されました。
ただしこれは、使いこなすのが難しい、組み込む時のコマンド:Vitalizeが面倒、モジュール利用前にvital#of('foo')などしないといけない(手軽に呼び出せない)といった欠点があります。

もっと手軽に簡易にライブラリを用意したい。そこで私は uptodate.vim たるものを作りました。*1

サマリー

  • 複数の同名のautoloadスクリプトの中から、常に最新版を利用するためのプラグイン。
  • スクリプトが読み込まれる際、 runtime! で全ての同名スクリプトが読み込まれる。
  • その中で一番最新のものが最後まで読み込まれ、そうでないものはファイル頭で読込が終了される。
  • これによってスクリプトは(たとえ同名のスクリプトが複数あったとしても)最新のものが読み込まれていることを保証できる。

すなわち複数のバージョンが混在していても常に最新版を読み込ませられるというわけです。

利用方法

uptodate.vimで管理できるのはautoload/以下のスクリプトのみです。
g:uptodate_filenamepatterns変数(リスト)を定義します。
これに管理したいオートロードスクリプトの、autoload以下のパスを入れたものを定義してください。autoload/lclib.vimというスクリプトを管理下に入れたいのであれば、以下のようにします。

let g:uptodate_filepatterns = ['lclib.vim']

次にそのスクリプト冒頭に以下の記述をします。

"UPTODATE: .
if lib#uptodate#isnot_uptodate(expand('<sfile>:p'))
  finish
endif

UPTODATE: .は更新ヘッダです。後に更新時刻を表す数字が書き込まれます。ここを見てファイルのバージョンを知ります。更新ヘッダはファイル先頭から35行以内*2に記述してください。
lib#uptodate#isnot_uptodate(expand(':p'))は現在読み込まれたファイルと同じパターンのファイルをruntime!して、もし今読み込んでいるファイルが最新版でないと判定したら1を返します。こうして最新版でないと判定されたらfinishされます。

そしてそのプラグインのautoload/lib/以下にautoload/lib/uptodate.vimのファイルをコピーすれば準備完了。
一度Vimを再起動するか、:UptodateResetting コマンドで plugin/uptodate.vimを再読み込みさせてください。

再び先ほどのライブラリ・スクリプトファイルに戻り、:write してみてください。
以下のように、更新ヘッダが更新時刻を表す数値に書き換われば設定成功です。

"UPTODATE: 1373476011.

ファイルを更新する度に、更新ヘッダの数字が更新されるのが確認できます。*3
そして、更新されるときに、'runtimepath'にある、他のlclib.vimも同時に現在の内容で更新されるようになります。これによって、自分の'runtimepath'内のlclib.vimは常に同じ内容であることが保証されます。

後はこのファイルのこれより下の部分にモジュールを定義します。

"UPTODATE: 1373476011.
if lib#uptodate#isnot_uptodate(expand('<sfile>:p'))
  finish
endif
" ここより以下にライブラリとして各モジュールを定義する
function! lclib#foo()
  " 何か処理
endfunction

動作

たとえばbar/autoload/以下と、baz/autoload/以下にlclib.vimをコピーした状態で、lclib#foo()を呼び出したとしましょう。
初めにどちらのスクリプトが呼ばれてもruntime! により結局両方のファイルが呼ばれることになります。
bar/autoload/lclib.vimの方が先に呼ばれたとします。
uptodateは UPTODATE: .ヘッダからbar/autoload/lclib.vimの更新時刻を保持します。
次にbaz/autoload/lclib.vimが読み込まれた時、baz/autoload/lclib.vimの更新時刻と保持されていたbarの更新時刻を比較し、barの方が新しかったり同じだったりするとlib#uptodate#isnot_uptodate(expand(':p'))は1を返すので、読込が終了します。そうでなければbaz/autoload/lclib.vimは最後まで読み込まれ、bar側のlib#uptodate#isnot_uptodate(expand(':p'))が1を返してbar/autoload/lclib.vimは読込が終了します。

最新版として読み込まれたファイルの情報は g:uptodate_loaded 変数に記述されます。確認する時はこれを参照してください。

注意

  • pathogenやvundleやneobundle.vimなどの、'runtimepath' をプラグインごとに通すプラグイン管理を前提にしています。昔ながらの、ダウンロードしたプラグインをvimfiles/以下に直接置く前時代的な管理には対応していませんので悪しがらず。
  • ライブラリ・スクリプトファイルを初めて呼び出した(読み込んだ)ときに全てが完結するようになっています。それ以後に新しく追加したファイルは感知しません。そういうときには:UptodateReload コマンドでスクリプトファイルの再読込を行ってください。このコマンドは引数で与えられたファイルパターンを再読込します。(引数が与えられていない時にはg:uptodate_filepatternsにある全てのパターンが読み込まれます。)
  • 常に最新版が読み込まれるので、古いバージョンでしか動かないようなプラグインをサポートしません。後方互換性を意識する必要があります。
  • 設計上、同名のファイルが増えれば(ファイル冒頭しか読まないとはいえ)読み込み時に時間がかかるでしょう。速度が気になりだしたら何らかの対策を講じるべきだと思います。

常に最新バージョンしか使えないという欠点はありますが、普段使っているautoload関数とまったく同じ感覚でモジュールを使えるのは便利です。
もともとはテストの中で使う汎用的な関数を呼ぶのに:Vitalizeなんかしてられるか!といった動機で作ったのですが、うまくいったようです。
そしてコマンドラインからも手軽に使えるようになったのも大きいです。今編集中のスクリプトのスクリプトIDを調べたいと思ったとき、コマンドラインから、

:echo lclib#get_script_id('%')

みたく呼ぶことが出来るのです。

*1:名付けにご協力してくださったujihisaさんに感謝

*2:この行数は将来変更する可能性があります

*3:ファイル更新時間の取得について、はじめはgetftime()を使っていたのだが、GitHubから取得したファイルは取得日時がそのまま更新時間となってしまうので上手くいかなかったために、ヘッダを使う方式にした。

git diffまとめ

全然 git diffを使いこなせなくて、使おうとする度にググるのが非効率に感じてきたのでメモ。

  • git diff
    • 無印はワークツリーとの比較。この場合ワークツリー(新)とインデックス(旧)
  • git diff HEAD
    • 無印のHEADなのでワークツリーとHEADを比較
  • git diff --cached HEAD
    • cachedはワークツリーなしの比較。つまりインデックスとの比較。この場合はインデックス(新)とHEAD(旧)
    • HEADを省略しようがしまいが--cachedの場合。
  • git diff {旧} {新}
    • {旧}と{新}を比較

一応ここにgit主要コマンドまとめてるけど見づらいしな