Awesome Hacks!

プログラミング初心者なので地道に勉強していきます。分からない人の立場から整理していきます。

シェルスクリプトの高速化

シェルスクリプトの高速化

業務でシェルスクリプトの高速化を追求する必要が出てきたので、整理。

シェルスクリプトノウハウ - モノノフ日記


ShellScript - shellで書かれたbatch scriptを手軽に高速化する - Qiita

ループ文のリダイレクト、パイプとバックグランドでの実行

Linuxコマンド集 - 【 wait 】 プロセスおよびジョブの終了を待つ:ITpro

grepするときにcatしないこと

まず、

cat file.txt | grep abc

より

grep abc file.txt

の方が早い。
 

②多重ループによる無駄な処理を回避すること

状況にもよるが多重ループが必ずしも早いとは限らない。
例えば毎行に対する処理を行う場合、単純にfor文で回して生成したカウンタを使って処理を実施するよりも、読み込んだ行に対してのみ処理を行いカウンタを使用して自分でカウントアップする方が早くなることもある。
 

③無関係な処理同士は極力バックグラウンドで

無関係な処理は立て続けにやる必要がないので並列処理にする。
なお、waitコマンドはすべてのバックグラウンドで実行しているプロセスの終了を待ってくれる。


これだけで大幅に高速化できた。

実際の確認

下記に実際のソースを記載する。
 

まず、3つのフィールドを持つcsv形式のランダムなファイルをテスト用に作成するためのプログラム

テスト用の入力ファイルを事前に作成するためのプログラム。
 
入力ファイルの定義条件を以下とする。
 ・第1フィールド : +[A〜I].
 ・第2フィールド : +[1〜4].
 ・第3フィールド : 0、または+[1〜999]
 ・上記の全ての組み合わせパターンでデータ(行)を作成し、行はランダムにソートする
 
【プログラム】

#!/bin/sh

F1_LINE=",+A.,+H.,+D.,+G.,+C.,+E.,+I.,+B.,+F.,"

cp /dev/null inFile.csv.tmp

for f1 in `echo $F1_LINE | sed -e "s/,/ /g"`
do
	for f2 in `seq 1 4`
	do
		for f3 in `seq 0 999`
		do
			if [ "$f3" != "0" ]
			then 
				f3="+$f3."
			fi
			echo "$f1,+$f2.,$f3," >> inFile.csv.tmp
		done
	done
done

cat inFile.csv.tmp | while read x; do echo -e "$RANDOM\t$x"; done | sort -k1,1n | cut -f 2- > inFile.csv

rm -f inFile.csv.tmp
exit 0


【生成されるファイルinFile.txtの内容(例)】※ファイルの並びはランダム

$ cat inFile.txt
+A.,+2.,+602.,
+B.,+1.,+571.,
+B.,+2.,+706.,
   ・
   ・
   ・
+E.,+4.,+873.,
+D.,+4.,+590.,
+A.,+3.,+763.,
$



上記を踏まえて、下記のルールでファイルを書き直す。
 ・第3フィールドを一意な値になるように振りなおす。
 ・ただし、元々第1フィールドと第3フィールドが同じ値であったものには全て同じ値が振られるようにする。
 

多重ループで立て続けに処理を行う場合

【プログラム】

$ cat loopTest.sh 
#!/bin/sh

F1_LINE=",+A.,+H.,+D.,+G.,+C.,+E.,+I.,+B.,+F.,"

cp /dev/null outFile.csv

CNT=0
for f1 in `echo $F1_LINE | sed -e "s/,/ /g"`
do
	/bin/echo -n "."
	for f3 in `seq 0 339`
	do
		if [ "$f3" != "0" ]
		then 
			f3="+$f3."
		fi
		cp /dev/null TMP_TEST_RONDOM.csv
		cat inFile.csv | grep ^"$f1,+[1-4].,$f3," > TMP_TEST.csv
		while read line
		do
			NEW_F3=$CNT
			if [ "$NEW_F3" != "0" ]
			then 
				NEW_F3="+$NEW_F3."
			fi

			echo $line | awk -F, -v t="$NEW_F3" 'BEGIN{OFS=","} END{$1=$1;$3=t;print}' >> TMP_TEST_RONDOM.csv
		done < TMP_TEST.csv
		sort TMP_TEST_RONDOM.csv -t, -k 2 >> outFile.csv
		CNT=$((CNT + 1))
	done	
done
rm -f TMP_TEST.csv
rm -f TMP_TEST_RONDOM.csv
echo ""

cp /dev/null outFile2.csv

CNT=0
for f1 in `echo $F1_LINE | sed -e "s/,/ /g"`
do
	/bin/echo -n "."
	for f3 in `seq 0 339`
	do
		if [ "$f3" != "0" ]
		then 
			f3="+$f3."
		fi
		cp /dev/null TMP_TEST_RONDOM.csv
		cat inFile.csv | grep ^"$f1,+[1-4].,$f3," > TMP_TEST.csv
		while read line
		do
			NEW_F3=$CNT
			if [ "$NEW_F3" != "0" ]
			then 
				NEW_F3="+$NEW_F3."
			fi

			echo $line | awk -F, -v t="$NEW_F3" 'BEGIN{OFS=","} END{$1=$1;$3=t;print}' >> TMP_TEST_RONDOM.csv
		done < TMP_TEST.csv
		sort TMP_TEST_RONDOM.csv -t, -k 2 >> outFile2.csv
		CNT=$((CNT + 1))
	done	
done
rm -f TMP_TEST.csv
rm -f TMP_TEST_RONDOM.csv
echo ""

exit 0
$ 


【実行結果】

$ time ./loopTest.sh 
 
.........
.........

real	19m8.678s
user	13m12.480s
sys	5m54.943s
$


読み込んだ行に対してのみ処理を行いカウンタを使用して自分でカウントアップする場合

【プログラム】

$ cat loopTest_speed_multi.sh 
#!/bin/sh

F1_LINE=",+A.,+H.,+D.,+G.,+C.,+E.,+I.,+B.,+F.,"

cp /dev/null outFile_speed_multi.csv

CNT=-1
FIELD3=""
for f1 in `echo $F1_LINE | sed -e "s/,/ /g"`
do
	/bin/echo -n "."
	grep ^"$f1" inFile.csv | sort -t, -k 3,3 > TMP_TEST2.csv
	while read line
	do
		if [ "$(echo $line | awk -F, '{print $3}')" != "$FIELD3" ]
		then
			CNT=$((CNT + 1))
			FIELD3="$(echo $line | awk -F, '{print $3}')"
		fi
		NEW_F3=$CNT
		if [ "$NEW_F3" != "0" ]
		then 
			NEW_F3="+$NEW_F3."
		fi
		echo $line | awk -F, -v "t=$NEW_F3" 'BEGIN {OFS=","} END{$1=$1;$3=t;print}' >> outFile_speed_multi.csv
	done < TMP_TEST2.csv
done &
rm -f TMP_TEST2.csv

cp /dev/null outFile_speed2_multi.csv

CNT=-1
FIELD3=""
for f1 in `echo $F1_LINE | sed -e "s/,/ /g"`
do
	/bin/echo -n "."
	grep ^"$f1" inFile.csv | sort -t, -k 3,3 > TMP_TEST2_multi.csv
	while read line
	do
		if [ "$(echo $line | awk -F, '{print $3}')" != "$FIELD3" ]
		then
			CNT=$((CNT + 1))
			FIELD3="$(echo $line | awk -F, '{print $3}')"
		fi
		NEW_F3=$CNT
		if [ "$NEW_F3" != "0" ]
		then 
			NEW_F3="+$NEW_F3."
		fi
		echo $line | awk -F, -v "t=$NEW_F3" 'BEGIN {OFS=","} END{$1=$1;$3=t;print}' >> outFile_speed2_multi.csv
	done < TMP_TEST2_multi.csv
done &
rm -f TMP_TEST2_multi.csv

echo ""

wait

exit 0
$ 

【実行結果】

$ time ./loopTest_speed_multi.sh 
.
.................
real	8m8.625s
user	5m56.679s
sys	8m40.820s
$ 


 

多重ループの方が速い場合とは

例えば、前記した「多重ループで立て続けに処理を行う場合」を「読み込んだ行に対してのみ処理を行いカウンタを使用して自分でカウントアップする場合」と同様にバックグラウンドにして条件を揃えたとする。
これでも「読み込んだ行に対してのみ処理を行いカウンタを使用して自分でカウントアップする場合」の方が速い。
しかし、もし入力ファイルの第3フィールドが0〜999ではなく0〜339であり、それに合わせてプログラムのループ回数も0〜339にした場合、「多重ループで立て続けに処理を行う場合」と「読み込んだ行に対してのみ処理を行いカウンタを使用して自分でカウントアップする場合」の実行時間は同じになる。
また、例えば0〜199など、339よりもっとデータ数が少ないと逆に「多重ループで立て続けに処理を行う場合」の方が処理終了までの時間は早くなる。
データ量に合わせて、多重ループで回すか、ファイルの読み込み行分だけ処理を行うか、処理を比較して適宜判断していくのがよさそうだ。