2010年9月23日木曜日

bashによる文字列置換が極端に非効率

bashには、文字列置換を行う機能が用意されており、便利に利用させてもらっています。しかし、これが非常に効率が悪いことがあることを知りました。サンプルプログラムは、次の通りです。
VAR の中の pattern を削除する ${VAR//patern} という書き方が遅い。
#!/bin/bash
# str_replace_perf.bash

#
# prepare 1000 strings of 6 digits
#
TEST_LIST=`seq 100100 100 200000`
echo $TEST_LIST | wc

#
# delete "150000"
#
T0=$SECONDS
A=${TEST_LIST//150000}
T1=$SECONDS
B=`echo $TEST_LIST | sed s/150000//g`
T2=$SECONDS

#
# results
#
echo T1-T0=$((T1-T0))
echo T2-T1=$((T2-T1))

# verify
echo $A | md5sum
echo $B | md5sum
このサンプルスクリプトでは、sed と速さを比較しています。
T1-T0 が bash で処理した場合の経過時間。T2-T1 が sed で同様の処理をした場合の経過時間。わたしのマシンでの実行結果は次の通りです。
[root@fedora14 ~]# cat /etc/redhat-release 
Fedora release 14 (Branched)
[root@fedora14 ~]# uname -r
2.6.35-0.57.rc6.git1.fc14.x86_64
[root@fedora14 ~]# bash --version
GNU bash, version 4.1.7(1)-release (x86_64-redhat-linux-gnu)
Copyright (C) 2009 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later 

This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
[root@fedora14 ~]# ./str_replace_perf.bash 
      1    1000    7000
T1-T0=10
T2-T1=0
de028ddb26bdd0b5aa062b1b8e753665  -
de028ddb26bdd0b5aa062b1b8e753665  -
上はサンプルですが、実際にこのボトルネックにハマって、修正が必要になった物件がありました。
Google で似たような話しが無いかはしばらく探したものの、見つかりませんでしたので、なんとか将来改善されることを期待して、bug-bash@gnu.orgへ連絡しました。

2010-09-25追記
早速、メンテナーの方から返事をもらいました。感謝。
多バイト文字サポートに起因するが、次のバージョン 4.2 に、修正を取り込む予定になっているとのことで、修正作業も既に終えているそうです。
なお、上記(多バイト文字サポートに起因)をヒントに、サンプルスクリプトを実行する前に、LANG=C を設定したところ、同じマシンでのスクリプト実行結果が T1-T0=1 に短縮されることを確認しました。
[root@fedora14 ~]# echo $LANG
ja_JP.UTF-8
[root@fedora14 ~]# ./str_replace_perf.bash 
      1    1000    7000
T1-T0=9
T2-T1=0
de028ddb26bdd0b5aa062b1b8e753665  -
de028ddb26bdd0b5aa062b1b8e753665  -
[root@fedora14 ~]# LANG=C
[root@fedora14 ~]# ./str_replace_perf.bash 
      1    1000    7000
T1-T0=1
T2-T1=0
de028ddb26bdd0b5aa062b1b8e753665  -
de028ddb26bdd0b5aa062b1b8e753665  -

2011-03-03追記
少し遅れましたが、先月 bash-4.2 がリリース済みであることを知り、さっそく試してみたところ、劇的に速くなっていました。メンテナー様、ありがとうございます。
なお、以前とは別のマシンで確認を行っています。
[root@fedora14 ~]# uname -a
Linux fedora14 2.6.35.10-74.fc14.x86_64 #1 SMP Thu Dec 23 16:04:50 UTC 2010 x86_64 x86_64 x86_64 GNU/Linux
[root@fedora14 ~]# echo $LANG
ja_JP.UTF-8
[root@fedora14 ~]# rpm -q bash
bash-4.1.7-3.fc14.x86_64
[root@fedora14 ~]# time ./str_replace_perf.bash 
      1    1000    7000
T1-T0=3
T2-T1=0
de028ddb26bdd0b5aa062b1b8e753665  -
de028ddb26bdd0b5aa062b1b8e753665  -

real    0m3.839s
user    0m3.799s
sys     0m0.020s
[root@fedora14 ~]# rpm -Uvh bash-4.2.0-2.fc15.x86_64.rpm 
警告: bash-4.2.0-2.fc15.x86_64.rpm: ヘッダ V3 RSA/SHA256 Signature, key ID 069c8460: NOKEY
準備中...                ########################################### [100%]
   1:bash                   ########################################### [100%]
[root@fedora14 ~]# time ./str_replace_perf.bash 
      1    1000    7000
T1-T0=0
T2-T1=0
de028ddb26bdd0b5aa062b1b8e753665  -
de028ddb26bdd0b5aa062b1b8e753665  -

real    0m0.028s
user    0m0.014s
sys     0m0.011s

2010年9月19日日曜日

オプション解析 (getoptsとgetoptの使い分け)

この話題はきっとポピュラーで、ネット上に多数の情報があるものと思いますが、、、

これまで、bash スクリプトでオプション解析をする際には、bash 組み込みコマンドのgetopts を使っていて、それで十二分と思っていました。しかし、getopts だと、コマンドラインの後半でオプションを指定できないことに気がつきました。 具体的には、

Usage: command.bash [-a] [-d dir] item1 item2 ...

のような構文の場合に、-d dir を item の後ろに指定しても、認識されません。(もちろん、工夫すればできそうではありますが)

そこで、調べたところ、外部コマンドの getopt なら期待通り処理してくれることを知りました。自分の中の整理のため、テンプレートを作りました。

まずは、getopts 版。いつもこのパターンを使ってました。
#!/bin/bash
#
# getopts-template.bash (use builtin getopts)
#
export LANG=C
usage_exit() {
        echo "Usage: getopts-template.bash [-a] [-d dir] item1 item2 ..." 1>&2
        exit 1
}
#
# Options
#
echo "$@"       ####DEBUG
while getopts ":ad:h" GETOPTS
do
  case $GETOPTS in
  a)    A_FLAG=yes
        ;;
  d)    OUTDIR=$OPTARG
        ;;
  h)    usage_exit
        ;;
  *)    usage_exit
        ;;
  esac
done
#
shift $((OPTIND - 1))
#
echo \$#=$#             ####DEBUG
echo \$@="$@"           ####DEBUG
echo A_FLAG=$A_FLAG     ####DEBUG
echo OUTDIR=$OUTDIR     ####DEBUG
次に、getopt 版です。
#!/bin/bash
#
# getopt-template.bash (use getopt utility)
#
export LANG=C
usage_exit() {
        echo "Usage: getopt-template.bash [-a] [-d dir] item1 item2 ..." 1>&2
        exit 1
}
#
# Options
#
echo "$@"       ####DEBUG
GETOPT=`getopt -q -o ad:h -- "$@"` ; [ $? != 0 ] && usage_exit
eval set -- "$GETOPT"
echo "$@"       ####DEBUG
while true
do
  case $1 in
  -a)   A_FLAG=yes      ; shift
        ;;
  -d)   OUTDIR=$2       ; shift 2
        ;;
  -h)   usage_exit
        ;;
  --)   shift ; break
        ;;
  *)    usage_exit
        ;;
  esac
done
#
echo \$#=$#             ####DEBUG
echo \$@="$@"           ####DEBUG
echo A_FLAG=$A_FLAG     ####DEBUG
echo OUTDIR=$OUTDIR     ####DEBUG
こちらの、getopt 版であれば、次のように -d オプションの指定を後半に行うことができます。
# ./getopt-template.bash item1 item2 -d dir
item1 item2 -d dir
-d dir -- item1 item2
$#=2
$@=item1 item2
A_FLAG=
OUTDIR=dir
一方、getopts の場合には、次のようになってしまいます。
# ./getopts-template.bash item1 item2 -d dir
item1 item2 -d dir
$#=4
$@=item1 item2 -d dir
A_FLAG=
OUTDIR=

2010-09-25追記
まとめを書き忘れていました。getopts のほうがスッキリすると思うので、itemが不要な場合(将来も不要かどうかも一考はしつつ)は、getoptsで書き、item 指定がある構文を使う場合には、getopt を使う。というガイドラインでやっていこうと思います。

2011-02-13追記
下記も参照ください。ロングオプションの場合です。
getoptによるロングオプション解析

2011-08-31追記
perl の場合についても書きましたので、興味があるようでしたら、参照ください。
perl でオプション解析

2012-05-20追記
Python の場合についても書きました。
Python でオプション解析

2010年9月12日日曜日

perl 5.12

少し遅れをとってしまいましたが、Fedora 14 Alpha を試しました。 このバージョンでは、perl が 5.12 にアップしていて、手持ちのスクリプトを試したら、一部動かなくなっていました。 調べたところ、split した結果が @_ に入ることを期待したコードがNGでした。ラクダ本VOLUME 2 28章 特殊変数によると、旧式の機能なので使うなとのこと。 実際にやっちゃっていたコードは次のうようなものです。
...
  split("\n", $temp) ;
  return @_ ;
}
これを、
...
  return split("\n", $temp) ;
}
と修正しました。今考えれば、なぜ、分割していたのかと思えますが、perl の理解が足りなかったということですね。
人気ブログランキングへ にほんブログ村 IT技術ブログへ