シェルプログラミングにおいて、ループカウンタなどをインクリメントするとき、どのようにしますか?
いきなりですがサンプルから。
#!/bin/bash
COUNT=0
while [ $COUNT -lt 1000 ]; do
# 何かの処理
COUNT=`expr $COUNT + 1` # COUNT をインクリメント
done
expr
コマンドを使う?
シェルプログラミングの入門記事などを見ると、変数のインクリメントに上記のような COUNT=`expr $COUNT + 1`
を用いているものが多くあります。
しかし、この書き方は とても遅い です。空のループを1000回繰り返すだけでも手元の mac (Core i7) で約2秒もかかってしまいました。
$ time bash shelltest.sh
real 0m1.939s
user 0m0.791s
sys 0m1.098s
二重括弧 ((...))
を使う!
Bash では expr
の代わりに二重括弧を用いて算術式を評価することができます。評価した値を取り出したいときは二重括弧の前に$
をつけて $((...))
とします。つまり、
(( COUNT++ )) # COUNT をインクリメント
または、少し明示的に
COUNT=$(( COUNT + 1 )) # COUNT をインクリメント
とします。こちらを使ったほうが 圧倒的に速い です。
最初のサンプルのインクリメント部分だけをこれに変えたプログラム
#!/bin/bash
COUNT=0
while [ $COUNT -lt 1000 ]; do
# 何かの処理
COUNT=$(( COUNT + 1 )) # COUNT をインクリメント
done
を実行してみると、
$ time bash shelltest2.sh
real 0m0.016s
user 0m0.014s
sys 0m0.002s
と、 100倍以上の速さ になっています。
汎用性?
いちおう、最初のexpr
を使う書き方のほうが汎用性が高いみたいです。Solaris とかのシェルでも使える?
そっち系のシェルでは二重括弧が使えないものがあるとか。
ただ、現在のところ広く使われている CentOS や、mac でもデフォルトのシェルが bash になっていますし、あまり調べていないけど他の zsh とかのシェルでも二重括弧はある程度使えそうなのかな?と言った印象です。(zsh では少なくとも上のスクリプトは実行できました。)
bashを使っているなら、 何が何でも汎用性を保ちたい!ということでなければ後者の二重括弧を使うほうがよい と思います。
expr
はなぜ遅いのか?
さて、今回の実験ではたった1000回のループしか行なっていないので二重括弧が速いというよりも expr
がインクリメントの処理としては尋常ではなく遅い 、というべきでしょう。これはなぜでしょうか?
コマンドだから fork が遅い
これもあまり詳しくは調べていないのですが、自分の理解の範囲で。
COUNT=`expr $COUNT + 1`
と、右辺をバッククォートでくくっていることから、 expr
はコマンド扱いですよね。実際、コマンドラインから expr
というコマンドを実行することもできますしね。
以前偉い人から聞いた話では、このような場合この expr
コマンドは別プロセスを立ちあげて実行される、とのことでした。つまり プロセスの fork
が必要です。fork
のコストは相当高いです。変数に1足すためだけにわざわざ別プロセスを fork するなんていかにも大げさですよね。牛刀を以ってなんとやら、な感じです。
Cygwin だとさらにやばい
昨今では使う人は減っているのかもしれませんが、Windows 上で Linux 環境をエミュレートする Cygwin では fork のコストがさらに桁違いらしいです。
いま手元にないので試せませんが、以前にやった時には普通の Linux に比べて10倍くらい遅かった覚えがあります。二重括弧式と比べると 1000倍の遅さ です!これはちょっと使っていられないでしょうね・・・