この記事は Shell Script Advent Calendar 2015 10日目の記事です。
9日目の記事はryoana14さんの麗しきawkの世界でした。
Shellスクリプトがいつまで経ってもまともに書けないMasWagです。書けないなりにも人の書いた(昔の自分が書いたものも多く含む)スクリプトを見てこれは遅いなと思うことはたまにあります。書き方のコツというか考え方が幾つかあると思うのでまとめてみようと思います。基本的な話なので多分Shellスクリプトをあんまり普段書かない人向けだと思います。ShellはBashを前提として書きます。zshだともっと色々できるのかもしれないです。細かい説明は(そんなに細かくなくても?)省いているので適宜manやinfoを参照すると良いでしょう。
forkを減らす
Shellでコマンドを使うということは多くの場合プロセスをforkしていることになります。Cygwinでプロセスを立てるコストが非常に高いということが問題になったりしますが、*nixでもプロセスを立てるコストは多くの場合無視できません。そうとなるとshellの組み込みの機能でできることはやりたくなります。 Bashの組込み機能で使用頻度が高そうなものを挙げていきます。
まず数式処理"(())"があります。これは大体exprの代替だと思って間違いないです。案外使える機能に((t++))でインクリメントできたり((t+=2))のようなものもあります。
下の結果を見ると解ると思いますが、やはりforkの回数を減らすと目に見えて速度が速くなります。一回だけ実行するタイプではbcもかなり健闘しています。
#!/bin/bash | |
s=0 | |
time for a in $(seq 0 1000); do | |
s=$(echo $s + $a | bc); | |
done | |
echo $s | |
s=0 | |
time for a in $(seq 0 1000); do | |
s=$( expr $s + $a); | |
done | |
echo $s | |
s=0 | |
time for a in $(seq 0 1000); do | |
((s += $a)); | |
done | |
echo $s | |
s=0 | |
time s=$(seq -s + 0 1000 | bc) | |
echo $s | |
s=0 | |
time s=$(expr $(seq -s " + " 0 1000)) | |
echo $s | |
s=0 | |
time s=$(($(seq -s + 0 1000))) | |
echo $s | |
s=0 |
real 0m2.002s | |
user 0m0.070s | |
sys 0m0.197s | |
500500 | |
real 0m1.308s | |
user 0m0.047s | |
sys 0m0.173s | |
500500 | |
real 0m0.007s | |
user 0m0.007s | |
sys 0m0.000s | |
500500 | |
real 0m0.003s | |
user 0m0.000s | |
sys 0m0.000s | |
500500 | |
real 0m0.006s | |
user 0m0.000s | |
sys 0m0.000s | |
500500 | |
real 0m0.002s | |
user 0m0.003s | |
sys 0m0.000s | |
500500 |
次にtest/[]の代替の[[]]があります。過去の記事にもあるように単にtestの代替として使うだけでもちょっと速くなりますが、実は[[]]は正規表現マッチができます。grepを使って正規表現マッチしている分岐があったら書き直した方がいいです。またこれは正規表現の話ですが、複数の正規表現のorで分岐するならひとつの正規表現にまとめた方がいいです。
下の結果のようにまあ[[]]を使うと速いです。testとの比較だとそこまで時間が変わっていないですがgrepとの比較だとかなり時間に差が出てしまいます。
#!/bin/bash | |
s=0; | |
# とても回数が多いことに注意 | |
time for a in {0..100000}; do | |
if [ $a == "0" ] ; then | |
((s++)); | |
fi | |
done | |
echo $s | |
s=0; | |
# とても回数が多いことに注意 | |
time for a in {0..100000}; do | |
if [[ $a == "0" ]] ; then | |
((s++)); | |
fi | |
done | |
echo $s | |
s=0; | |
# 回数は減らしました | |
time for a in {0..100}; do | |
if echo $a | grep "0" > /dev/null; then | |
((s++)); | |
fi | |
done | |
echo $s | |
s=0; | |
# 回数は減らしました | |
time for a in {0..100}; do | |
if [[ $a =~ 0 ]] ; then | |
((s++)); | |
fi | |
done | |
echo $s | |
s=0; | |
# 回数は多いです | |
time for a in {0..100000}; do | |
if [[ $a =~ 0 ]] || [[ $a =~ 2 ]]; then | |
((s++)); | |
fi | |
done | |
echo $s | |
s=0; | |
# 回数は多いです | |
time for a in {0..100000}; do | |
if [[ $a =~ 0 || $a =~ 2 ]]; then | |
((s++)); | |
fi | |
done | |
echo $s | |
s=0; | |
# 回数は多いです | |
time for a in {0..100000}; do | |
if [[ $a =~ 0|2 ]]; then | |
((s++)); | |
fi | |
done | |
echo $s |
real 0m0.686s | |
user 0m0.677s | |
sys 0m0.007s | |
1 | |
real 0m0.367s | |
user 0m0.363s | |
sys 0m0.003s | |
1 | |
real 0m0.608s | |
user 0m0.327s | |
sys 0m0.113s | |
11 | |
real 0m0.001s | |
user 0m0.003s | |
sys 0m0.000s | |
11 | |
real 0m1.299s | |
user 0m1.277s | |
sys 0m0.017s | |
62553 | |
real 0m1.189s | |
user 0m1.163s | |
sys 0m0.023s | |
62553 | |
real 0m1.028s | |
user 0m1.007s | |
sys 0m0.017s | |
62553 |
最後に変数展開時の文字列操作があります。変数に対してsedとかを使いたくなったらこっちを使えないか考えるといいと思います。幾つか種類があるので詳細はmanやinfoを参照してください
#!/bin/bash | |
a="a.out" | |
time for i in {0..10000}; do | |
echo $a; | |
done | uniq | |
time for i in {0..100}; do | |
echo ${a%.*}; | |
done | uniq | |
time for i in {0..100}; do | |
echo $a | sed 's/\..*//'; | |
done | uniq | |
time for i in {0..100}; do | |
echo ${a/out/c}; | |
done | uniq | |
time for i in {0..100}; do | |
echo $a | sed 's/out$/c/'; | |
done | uniq |
a.out | |
real 0m0.113s | |
user 0m0.130s | |
sys 0m0.023s | |
a | |
real 0m0.002s | |
user 0m0.000s | |
sys 0m0.000s | |
a | |
real 0m0.475s | |
user 0m0.290s | |
sys 0m0.070s | |
a.c | |
real 0m0.003s | |
user 0m0.000s | |
sys 0m0.000s | |
a.c | |
real 0m0.478s | |
user 0m0.307s | |
sys 0m0.060s |
pipeで繋げられるものは積極的に繋ぐ
Shellスクリプトでコマンドをpipeでつなげると、マルチプロセスで前の処理がすべて終わるのを待たずに次の処理を行うことになるので大抵速いです。特にこのご時世マルチコアなので本当に並列化ができて速いです。良い時代になりました。
専用コマンドがあったらそれを使う
専用コマンドがあったらそれを使うというのはシンプルですが有効な方法です。Shellスクリプトで使うコマンドは基本的にCで実装されているものが多く、まあ速いです。機能が少ない(用途に特化している)物のほうが大抵速いので専用のコマンドがあればそっちを使ったほうがいいです。例えば高機能なcutとしてawkを使うことはあると思いますが、cutで足りる場合にはcutの方が速かったりします。awkが悪者みたいですがshellでloopの処理があったときにawkに置き換えて高速化できることは良くあります。
while readはawkで書きなおしたほうが大体速い(こんなこと書くと怒られうる)
あんまり滅多なこと書くとShell界隈のいろんな人に怒られますが、while readはawkで書きなおしたほうが簡潔にかけて速いことが多いと思っているので僕はawkを使います。僕は基本的にawkが好きな人です。awkはShellスクリプトではない派の人もいるので気をつけましょう。while内部の変数は扱いに注意が必要なので僕はawkを推します。
$ time seq 100000 | awk '$0=(t=$1+t)' | tail -n 1
5000050000
real 0m0.102s
user 0m0.103s
sys 0m0.007s
$ time seq 100000 | while read a ;do echo $((t+=$a));done | tail -n 1
5000050000
real 0m1.712s
user 0m1.490s
sys 0m0.557s
11日目はzayarwinttunさんです。
シェルの while read が遅いのは、read コマンドが 1バイトずつ read(2) するのが大きな要因です。大量なデータ相手には辛い。
返信削除