[shell script][bash] 同じ入力で複数処理をしたい

たいした話ではないですが、何度も忘れて書けなくなるのでメモ。

ある入力に対して複数の処理をしたいとき、入力がただのファイルだったら、まあcatを2回やりゃあいいんですが、プログラムの出力だったり、ファイルが圧縮されててbzcatしなきゃいけなかったりだとちょいと面倒です。入力の量がたいしたことなければいったんメモリに書いてあげればよくて、多くて大変だったら一時ファイルに書いてあげれば(その後の試行錯誤も見越すと)いいかと。

とはいうものの、ワンライナーでさくっとやってしまいたいときに、ゴミファイルが散乱するのも気持ち悪いので、pipe(2)してdup2(2)して楽をしたいですよね。というかほんとに楽できてるのかな? と思ったので、軽く調べてみました。

お題

1から10,000,000までの数字のうち、123と456を含むものの数を求めよ。

fizz buzzにしようかと一瞬思ったけど、ぜんぜん関係なかった。

解答例

  • いちばんナイーブな方法 (naive)
    • seq がものすごい時間かかる処理の場合、倍の時間がかかってしまう。
seq 10000000 | grep 123 | wc -l
seq 10000000 | grep 456 | wc -l
  • いったん変数に代入 (memory)
    • $aho がすごいメモリ食う
aho=$(seq 10000000)
echo "$aho" | grep 123 | wc -l
echo "$aho" | grep 456 | wc -l
  • 名前付きパイプ (pipe)
    • きっとこれが一番賢いと思う。けどteeがいまいちな気もする。
seq 10000000 | tee >/dev/null >(grep 123 | wc -l) >(grep 456 | wc -l)

実行結果の違い

% while :; do ps -e -ouser,comm,pid,start,etime,rss | grep nobody; sleep 1; done | tee aho

みたいなかんじでかなり適当な統計ですが、こんな感じになりました。

naiveがpipeのちょうど2倍くらい時間かかって、メモリはnaiveが3M, pipeが6M程度。seq 1千万だとmemoryは1GB近くメモり食う上に一番遅い、ってとこでした。

まとめ

seqがbzcatみたいに遅くて、さらに一時ファイルに書くI/Oももったいないくらいなときは、named pipeが良さそうですね。

ちなみにset -xしてあげるとこんな感じになります。

ichii386% time bash -c 'set -x; seq 10000000 | tee >/dev/null >(grep 123 | wc -l) >(grep 456 | wc -l)' 
+ seq 10000000
++ grep 123
++ grep 456
+ tee /dev/fd/63 /dev/fd/62
++ wc -l
++ wc -l
49970
49970
bash -c   7.52s user 0.35s system 99% cpu 7.875 total

やっぱりteeが気持ち悪いす。もっといい方法ご存知の方はおしえてください...。