最近になって、 GNU Coreutils の split(1) に --filter ってオプションがあり、入力を並列処理する方法の1つになることを知りました*1。
並列処理をしたいときに使うものとして GNU Findutils の xargs(1) -P, --max-procs (おそらく GNU 拡張) と、そのままの GNU parallel があります。サーバ管理で並列にログインしてなんかやる系だともっと多くのバリエーションが有るでしょうが、 synax sugar の域を超えないのでここでは考えません。
尤も split も本来の目的がちょっと違うので同じように比較するのはおかしいんですが、思ったより効率が良いようなので試してみました。
環境は SunOS, Xeon E5-2630 v3 @ 2.40GHz の 2 socket で 32 threads というとこです。
それぞれの実装
- xargs
- --max-procs の数まで fork して execvp する実装
ichii386@abby% xargs --version xargs (GNU findutils) 4.5.14 Copyright (C) 2014 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>. This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Written by Eric B. Decker, James Youngman, and Kevin Dalley.
- parallel
ichii386@abby% parallel --version GNU parallel 20121122 Copyright (C) 2007,2008,2009,2010,2011,2012 Ole Tange and Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. GNU parallel comes with no warranty. Web site: http://www.gnu.org/software/parallel When using GNU Parallel for a publication please cite: O. Tange (2011): GNU Parallel - The Command-Line Power Tool, ;login: The USENIX Magazine, February 2011:42-47.
- split
- 一般的な使い方は「ファイルを n 分割する」ことですが、 --filter を指定することで出力ファイルたちを open(3) するのではなく fork して SHELL を execl(3) し pipe を繋げます。
- それゆえ xargs などと違って「最初に n プロセスだけを起動し、入力を分割して渡す」という方針。
- -n r/N で入力を N 個の出力に round robin させる (よって入力の終端は起動時には不要)
ichii386@abby% split --version split (GNU coreutils) 8.23 Copyright (C) 2014 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>. This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Written by Torbjorn Granlund and Richard M. Stallman.
ということで、ナイーブには a. を改善しようとして b. or c. になるところ、いやいやプロセス起動が無駄だろ、と思いつつ d. にするのが調整めんどいよね、というときに良いようです。
- a. 並列度1で1の入力を処理する、を100回やる
- b. 並列度1で100の入力を処理する、を1回やる
- 複数の入力を一度に処理できるようにする
- c. 並列度5で1の入力を処理する、を20回やる
- xargs 的な方針
- d. 並列度5で20の入力を処理する、を1回やる
- split -n r/N --filter の方針
たんに起動するだけ
それぞれの得意分野だけ比較するのもずるいので、以下の条件で統一するようにします。
- /bin/sh は必ず経由する
- 入力を引数として何かを実行できる
ichii386@abby% seq 10e3 | ptime xargs -I{} -P32 sh -c '/bin/true {}' real 10.983304378 user 8.317562721 sys 24.905662776
ichii386@abby% seq 10e3 | SHELL=/bin/sh ptime parallel -k -j 32 /bin/true {} real 29.879385199 user 21.170876665 sys 53.838705772
ichii386@abby% seq 10e3 | SHELL=/bin/sh ptime split -u -n r/32 --filter 'while read i; do /bin/true $i; done' real 1.055647177 user 3.119058968 sys 10.104064297
/bin/true が 10e3 回実行されるのは同じですが、こんなに差がでます。ほんとか??
素因数分解ぽい何か
openssl genrsa 32 で作った 2849443549 = 52223 * 54563 が確かにこれで素因数分解できる感を確認する。
ichii386@abby% seq 2 52223 | ptime xargs -I{} -P32 sh -c 'expr 2849443549 % {} != 0 >/dev/null || echo {}' 52223 real 1:00.233418446 user 1:00.874658739 sys 3:06.241644775
ichii386@abby% seq 2 52223 | SHELL=/bin/sh ptime parallel -k -j 32 'expr 2849443549 % {} != 0 >/dev/null || echo {}' 52223 real 2:49.511349011 user 2:08.351005238 sys 5:23.125255516
ichii386@abby% seq 2 52223 | SHELL=/bin/sh ptime split -u -n r/32 --filter 'while read i; do expr 2849443549 % $i != 0 >/dev/null || echo $i; done' 52223 real 5.541663743 user 25.044178700 sys 1:17.303706104
圧倒的すぎて不安になってきます。
まとめ
とにかく圧倒的に split が速いです。正直なんでこんな差がでるのかピンときてません (が、ちゃんと調べてません) 。
parallel なんかは親が重たい図体してるくせに安易に fork してるからってのはあるでしょうけど。なんとなく気軽に xargs 使っているなら考えなおしたほうが良いかも。
(こういう用途で) split に唯一弱点があるとすれば、起動したコマンドが exit -1 した時に異常終了できないことです。
上の素因数分解の例で言えば、xargs の場合 seq inf を入力にしつつ "(echo {}; exit -1)" すれば見つかったところで終了してくれます。それが split だと出来ない。