たごもりすメモ

コードとかその他の話とか。

xargs を使ってカジュアルに並列処理

シェルからでも重い処理というのはちょこちょこあって、例えば超デカいログファイルを移動して圧縮したりというお仕事は世界中のあらゆる場所で毎日行われていたりする。コマンドラインからでも大量の圧縮済みログファイルをいっぺんに展開したい、とか。

あるディレクトリ以下に存在するたくさんのファイルを(圧縮済みのものを除いて)全部 bzip2 圧縮したい!と思ったら、とりあえずさくっと次のようにコマンドラインで叩けばいい。

$ find . -not -name '*.bz2' | xargs bzip2

これで、まあそんなに問題なく効率的にbzip2圧縮ができる。だがしかし。

最近は複数コアのCPUが普通に転がってるし、あまつさえHyperThreadingが有効になってたりしてOSから見える論理CPU数がハンパない。普通に8とかある。その一方で複数コアを使用してくれるコマンドというのはあんまりなくて、実際上のようなコマンドラインを普通に叩くと、4Core HTのマシンでは次のように実にもの悲しい処理状況になってしまう。

実に悲しい。どう悲しいか分からない人は、えー、残念でした。

CPUは余ってるので大量のファイルを並列処理したい、という場合、手でやるならまあ複数の端末を開いてそれぞれで実行してもいい。が、それはそれでどのファイルをどの端末で処理するのとか考えるのも面倒だ。それにできれば、シェルスクリプト中でも可能な方法が欲しい。

xargs の -P (--max-procs) オプションを使う

ところでxargsには -P というオプションがある(ことに今日気付いた)。詳しくは各自 man xargs していただきたいが、なんと複数プロセスを並列に実行してくれるらしい*1。ぐれいと!

が、これは xargs の動作をちゃんと理解していないと、いきなり以下のようにやってもマトモに動かない。

$ find . -not -name '*.bz2' | xargs -P 2 bzip2

期待としては2並列実行されて半分の時間で完了するってところだろうけど、これはその期待通りには動かない。たぶん最初の例と同じように1CPUだけ使って動作し、同じような時間で完了する。

xargsの動作について少し

xargsの動作に詳しくない人のためにいちおう復習。xargsは以下のように動作する。

  1. 標準入力から次々と行を読み込む
    • 終端に達するか、コマンドに与えられる引数*2に等しい数に達するまで続ける
  2. 読んだデータを引数に与えられたコマンドを起動する
  3. 標準入力からまだ読めるデータがあれば 1. に戻る

で、この「コマンドに与えられる引数」だが、けっこう大きい。以下のようにすればあなたの手元のシステムでのサイズがわかる。(80000は振り切れちゃう場合には適当に大きくすること。)

$ yes | head -80000 | xargs perl -e 'print scalar(@ARGV),"\n";'

Mac OSX 10.6 で実行すると 5000 で、CentOS5.6でやってみたら 16379 だった*3。自宅のDebian(testing kernel 2.6.28.7)だと 65519。

つまり何も考えずに xargs を実行すると、この数に達しない限りは1回のコマンドで実行されちゃうのだ。カジュアルな並列実行に壁が立ちはだかる。

xargs -L オプションを併用

で、ちゃんと偉い人が考えてくれていてありがたいことに xargs には -L というオプションがある。これを指定すればコマンド1回の実行にいくつの引数を(最大で)与えるかが指定できる。
このため、最悪ケースでも以下のようにすれば、常にふたつの bzip2 コマンドが並列で走って(だいたい)倍速で圧縮が完了するというわけだ。

$ find . -not -name '*.bz2' | xargs -L 1 -P 2 bzip2

ただし注意点。xargsで起動されるコマンドが軽く、かつ xargs 経由で食わせる引数の数が膨大なものとなる場合、並列実行によるメリットよりもコマンド起動回数(fork回数)によるデメリットの方が上回る可能性が高い。そういう場合には -L で指定する数を多くするなどして対処しよう。

実際のところ -L 1 という指定はほとんどの場合ナンセンスだと思う。コマンド実行時に「引数がどのくらいになりそうか」「どのくらい並列で実行していいか」を考えて、まあテキトーに -L -P それぞれ調整するとよろしい。*4

あらためて並列実行を確認

xargs による並列実行が行われていることをもう一度さくっと確認しておこう。-P のあるなしで以下の動作の違いを見ればイッパツ。

$ yes | head -100 | xargs -L 50 perl -e 'print "start!\n"; sleep(1); print scalar(@ARGV),"\n";'
start!
50
start!
50
$ yes | head -100 | xargs -L 50 -P 2 perl -e 'print "start!\n"; sleep(1); print scalar(@ARGV),"\n";'
start!
start!
50
50

これならシェルスクリプトからでも簡単に重い処理が並列実行できるぜ! やったやった!

(追記) See also: GNU Parallel

そういえばちょっと前にこんなエントリが上がってて自分もへーっと読んでました。
GNU Parallelがすごすぎて生きるのがつらい - As a Futurist...
xargs -P はたいていの環境でさくっと実行できて便利だけど、細かい制御とかもちろん全然効かないので、厳密にコントロールされた並列処理をやりたいなら GNU Parallel は良さそうだなーと思います。

*1:デフォルトは -P 1

*2:システムにより異なる

*3:'perl', '-e', およびスクリプトを加えて 16382 == 2^14 - 2

*4:-P 0 とかすると「可能な限り最大に並列実行」とかやってくれるみたいだけど、他の処理のために空きCPUを残しておくのは大事だと思うんだ……