シェル芸処理速度向上のヒント
先日のシェル芸勉強会ではWebサーバのログを扱った。ファイルサイズは約356MB、約350万行程度のそれなりに大きなテキストデータだった。このくらい大きなデータになると処理速度が気になってくるところ。
ではどんなところに気をつけるといいのか?下記の2点に注目して試してみることにした。
マルチコアの恩恵を受ける
一つのコマンドで頑張るより、パイプで刻んで複数のコマンドで処理した方が早くなる場合がある。何でもかんでも早くなるわけでは無い。パイプの流れが途中で止まら無いことが重要。
例えばsort処理は一度データを全てメモリに取り込む必要があるため、流れを止めてしまう。またパイプの最初で処理速度が遅いと、後ろのコマンドが手持ち無沙汰で遊んでしまうので遅くなる。
高速なコマンドで先にデータを絞り込む
パイプの流れを途中で止めないためには、先に高速に処理できるコマンドを持ってくる。ここでは高速処理出来るコマンドとして、GNU grepとmawkを使ってみる。
GNU grepは第15回シェル芸勉強会で扱ったが、とにかく速い。
【問題と解答例】第15回ドキッ!grepだらけのシェル芸勉強会 – 上田ブログ
先日買った「シェルプログラミング実用テクニック」にも、GNU grepの高速性について言及してあった。
シェルプログラミング実用テクニック電子書籍版のまとめ – 上田ブログ
mawkについては、シェル芸界隈で話題になっていたので気になっていたのでインストールした。
http://gauc.no-ip.org/awk-users-jp/blis.cgi/DoukakuAWK_287
http://gauc.no-ip.org/awk-users-jp/blis.cgi/DoukakuAWK_072
Macだったら下記のコマンドでインストールしてみよう。
$ brew install mawk
扱うデータは、今回も先日のシェル芸勉強会で使ったNASAのWebサーバログ。日付と時刻の正規化データは付けていないそのままのデータでやってみた。お題はこちら
お題
HTTPコード200の場合で、アクセス元ホスト毎に通信量を集計。
ということで、ログの最後が 200 数値(通信量)
となっている行のみを取り出し、アクセス元ホスト毎に足し算する。具体的には下記のような行について、行末の数値(通信量)を集計する。
host1.example1.co.jp - - [01/Jul/1995:00:00:01 -0400] "GET /history/apollo/ HTTP/1.0" 200 6245 192.168.7.5- - [01/Jul/1995:00:00:06 -0400] "GET /shuttle/countdown/ HTTP/1.0" 200 3985
集計結果は並べ替えはしない。最後の出力結果の例はこちら。
host1.example1.co.jp 10860630 hostaaa.example2.com 301305 hostbb.example3.edu 24169 192.168.7.5 352741 192.168.18.172 19748
それでは、集計を実際にやってみる。環境はいつものMacbook Air。プロセッサはIntel Core i7 1.7GHz、メモリは1600MHz DDR3 8GB。
Ruby単体
個人的にRubyが好きなので、まずはRubyから。UTF-8以外のデータがあると止まってしまうので、変なデータが入った行は事前に取り除いてある。
$ time < access_log.nasa.ascii ruby -anle 'BEGIN{a=Hash.new(ifnone=0)};a[$F[0]]+=$F[$F.size-1].to_i if $F[$F.size-2]=="200" && $F[$F.size-1]!="-"; END{a.each_pair{|k,v| puts "#{k} #{v}"}};' > /dev/null real 0m16.295s user 0m15.872s sys 0m0.325s
Ruby頑張った。これでもPerlやPythonに比べるとかなり高速に処理している。
GNU awk単体
次にGNU awk単体で。Rubyよりかなり高速化している。GNU awkのパフォーマンス恐るべし。
$ time < access_log.nasa gawk '$(NF-1)=="200" && $NF!="-"{a[$1]+=$(NF)}END{for(v in a){print v,a[v]}}' > /dev/null real 0m6.683s user 0m6.165s sys 0m0.242s
GNU grep + GNU awk
超高速処理が可能なGNU grepで先にデータを絞り込んでから、パイプでGNU awkに渡す。GNU awk単体より高速化しているのが分かる。
$ time < access_log.nasa LANG=C ggrep '200 [0-9]*$' | gawk '{a[$1]+=$(NF)}END{for(v in a){print v,a[v]}}' > /dev/null real 0m4.108s user 0m6.311s sys 0m0.260s
GNU grep + mawk
最後にGNU awkの代わりにmawkを使うと、更なる高速化が出来る。この組み合わせは要チェックや!
$ time < access_log.nasa LANG=C ggrep '200 [0-9]*$' | mawk '{a[$1]+=$(NF)}END{for(v in a){print v,a[v]}}' > /dev/null real 0m2.588s user 0m4.639s sys 0m0.255s
mawk単体
参考としてmawk単体の結果も。mawkの処理は整数値の数値演算があると特に高速化されるらしい。
$ time < access_log.nasa mawk '$(NF-1)=="200" && $NF!="-"{a[$1]+=$(NF)}END{for(v in a){print v,a[v]}}' > /dev/null real 0m3.735s user 0m3.503s sys 0m0.201s
sortしてサムアップ
もひとつ参考に、sortしてOpen usp Tukubaiのsm2コマンド(Haskell版)で集計の場合。この場合、GNU sortコマンドはLANG=Cや-sオプション使って、可能な限り高速化の工夫はしており、gsortまではrealが5秒くらい。sm2の処理がネックになっている。商用版のsm2ならば恐らく速いのだろう。ただ、Haskell版のsm2は、Python版に比べて速いので重宝している。ありがたや。
$ time < access_log.nasa LANG=C ggrep ' *200 [0-9]*$' | mawk '{print $1,$NF}' | LANG=C gsort -s -k1,1 | sm2 1 1 2 2 > /dev/null real 0m23.034s user 0m25.117s sys 0m0.852s