日々之迷歩

世の中わからんことだらけ

ITが複雑で難しくなっていく様に翻弄される日々です

シェル芸処理速度向上のヒント

先日のシェル芸勉強会ではWebサーバのログを扱った。ファイルサイズは約356MB、約350万行程度のそれなりに大きなテキストデータだった。このくらい大きなデータになると処理速度が気になってくるところ。

ではどんなところに気をつけるといいのか?下記の2点に注目して試してみることにした。

マルチコアの恩恵を受ける

一つのコマンドで頑張るより、パイプで刻んで複数のコマンドで処理した方が早くなる場合がある。何でもかんでも早くなるわけでは無い。パイプの流れが途中で止まら無いことが重要。

例えばsort処理は一度データを全てメモリに取り込む必要があるため、流れを止めてしまう。またパイプの最初で処理速度が遅いと、後ろのコマンドが手持ち無沙汰で遊んでしまうので遅くなる。

高速なコマンドで先にデータを絞り込む

パイプの流れを途中で止めないためには、先に高速に処理できるコマンドを持ってくる。ここでは高速処理出来るコマンドとして、GNU grepとmawkを使ってみる。

GNU grepは第15回シェル芸勉強会で扱ったが、とにかく速い。

【問題と解答例】第15回ドキッ!grepだらけのシェル芸勉強会 – 上田ブログ

先日買った「シェルプログラミング実用テクニック」にも、GNU grepの高速性について言及してあった。

シェルプログラミング実用テクニック電子書籍版のまとめ – 上田ブログ

mawkについては、シェル芸界隈で話題になっていたので気になっていたのでインストールした。

AWK Users JP :: awk のパフォーマンス

AWK Users JP :: 10 億回のループ

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