findとgrepの合わせ技

カーネルなど大規模なソースコードを検索したい場合、findとgrepを使ったり、タグジャンプを使うだろう。Eclipse使う場合とか異論はあると思うけど、今日はfindとgrepの合わせ技について調べてみた。

普段はxargsを噛ませて、

$ find . -name \*.c | xargs grep hogehoge

とかやるんだけど、findの-execオプションを使った場合とどれくらい性能差があるのだろうか? 評価環境はVMWare Fusion上のUbuntu 11.04。Linux kernel 2.6.28.10のfs以下で、まずは次の3パターンを比較してみた。3回測定してtimeの最良値を示す。おそらく対象ファイルはファイルキャッシュに載っている思う。

$ time find . -name \*.c -exec grep wait_event_interruptible {} /dev/null \;
  :
real    0m10.314s
user    0m3.080s
sys     0m3.736s

$ time find . -name \*.c -print | xargs grep wait_event_interruptible /dev/null
  :
real    0m0.123s
user    0m0.020s
sys     0m0.044s

$ time find . -name \*.c -print0 | xargs -0 grep wait_event_interruptible /dev/null
  :
real    0m0.106s
user    0m0.008s
sys     0m0.052s

find -execよりもxargsを使った方が10倍程度速い。また、区切りを空白ではなく、ヌル文字にした方が若干速い*1 *2。find -execとxargsの速度差はgrepの実行回数に起因する。find -execの場合はマッチ文字列(ファイル名)ごとにgrepを実行するのに対して、xargsの場合はその入力から部分リストを作ってgrepの引数に渡す。今回程度の規模だとgrepの実行回数は1回で済む。

さて、今回初めて知ったのだが、最近のfindは(Mac OS Xでも対応しているので、GNUだけでなくBSDでも使えるようだ)、-execオプションの最後に「+」と書くことでxargs同様の挙動になるようだ。速度の方も当然速くなる。

$ time find . -name \*.c -exec grep wait_event_interruptible {} +
  :
real    0m0.095s
user    0m0.028s
sys     0m0.028s

これまでで最速の結果が得られた。これからしばらくこの方法を使ってみるかな。

xargsには-Pオプションってのがあって、複数プロセスを起動する方法もある。マルチコア環境だと、効果があったりするかな? 今回の実験環境には1コアしか割り当ててないので、意味がないと思うけど、そのうち試してみるかな。

ちなみに、Plan 9ではfindもxargsもないので、コマンド置換(`{command})を使って次のように書くのが一般的かな。

% grep -n pattern `{du -a | awk '{print $2}'}

Plan 9にもARG_MAXの制限はあるのかな?

(追記:2011-05-08)山下さんの指摘を受け、xargsの実行時間も入るように測定し直した。結果、実行時間(real)に大差はない。find側が子プロセスの終了をwaitするから、まぁそうなるのは当たり前か。

$ time sh -c 'find . -name \*.c -exec grep wait_event_interruptible {} /dev/null \;'
  :
real    0m10.641s
user    0m2.984s
sys     0m4.076s

$ time sh -c 'find . -name \*.c -print | xargs grep wait_event_interruptible /dev/null'
  :
real    0m0.110s
user    0m0.036s
sys     0m0.020s

$ time sh -c 'find . -name \*.c -print0 | xargs -0 grep wait_event_interruptible /dev/null'
  :
real    0m0.107s
user    0m0.016s
sys     0m0.044s

$ time sh - c 'find . -name \*.c -exec grep wait_event_interruptible {} +'
  :
real    0m0.108s
user    0m0.012s
sys     0m0.036s

*1:誤差程度かもしれないけど。ファイル名に空白が含まれている場合もあり得るので、-print0を使う方がお勧め。

*2:/dev/nullを付け加えているのは、grepでファイルが一つしかマッチしない場合でもファイル名を表示するようにするおまじない。