シェルで変数のインクリメントに expr を使うと100倍遅い件

  • 181
    いいね
  • 7
    コメント
この記事は最終更新日から1年以上が経過しています。

シェルプログラミングにおいて、ループカウンタなどをインクリメントするとき、どのようにしますか?

いきなりですがサンプルから。

#!/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倍の遅さ です!これはちょっと使っていられないでしょうね・・・

参考