手軽にスクリプトを並列実行する(シェルスクリプト)

課題

単一の画像に対してある画像処理行うをスクリプトPythonで作成した。これを大量の画像に対して実行すると長時間が必要になる。

解決策

GNU parallelを利用して並列実行すると高速になる。parallelはコマンドやスクリプトを並列実行するためのコマンドである。入力列の行単位で並列実行する。

具体的な例を見てみる。OpenCVのテンプレートマッチングを利用して、ターゲット画像中にテンプレート画像と類似度が高い部分が存在するかどうかを判定するPythonスクリプトを用意した(is_match.py)。このスクリプトは、類似度が高い部分が存在するとき、その座標を標準出力に出力し、終了コードゼロで終了する。そのほか詳細な仕様については割愛する。

今回はテンプレート画像1個とターゲット画像100個を対象として処理を実行してみる。つまり、100個の各ターゲット画像に対してテンプレートマッチングを実行する。具体的には次のような画像を使用した。

ターゲット画像(lena

テンプレート画像(lenaの目)

テンプレート画像はターゲット画像からクロップして生成した(512x512の大きさのターゲット画像から座標[240,240]の位置で48x48の大きさでクロップ)。また100個のターゲット画像は元の画像をコピーしたものである。そのためテンプレートマッチングの結果はすべて同一(成功)となる。ただし今回は実行時間のみに注目する。

まずは愚直に100個のテンプレートマッチングを直列に実行する場合を示す。以下のスクリプト(run_sequential.sh)を用意した。ただし、targetディレクトリに100個のターゲット画像(lena_0.pnglena_1.png、...)が存在するとする。また、後半のsedコマンドは、実行対象となったターゲット画像名を表示するために付加した。

find 'target' -name "lena_*.png"                     |
while read -r target
do
  ./is_match.py './template/eye.png' "${target}"     |
  sed 's!^!'"${target}"':!'
done

実行時間を計測するために time コマンドを経由してこのスクリプトを実行した。

$ time ./run_sequential.sh 
target/lena_45.png:240 240
target/lena_51.png:240 240
target/lena_79.png:240 240
(略)
target/lena_74.png:240 240
target/lena_60.png:240 240
target/lena_48.png:240 240

real    0m9.827s
user    0m7.831s
sys 0m1.607s

次に100個のテンプレートマッチングを並列に実行する場合を示す。以下のスクリプト(run_parallel.sh)を用意した。parallelの引数に現れる「{}」は標準入力の各行のプレースホルダになってることに注意したい(parallelの文法)。つまり、各行の内容(find した結果 = lena_x.pngのパス)がこの「{}」に次々と埋め込まれてスクリプトが実行されていくということである。

find 'target' -name "lena_*.png"                                |
parallel './is_match.py ./template/eye.png {} | sed "s!^!{}:"!'

こちらも同様にtimeを利用して実行時間を計測した。

$ time ./run_parallel.sh 
target/lena_86.png:240 240
target/lena_92.png:240 240
target/lena_45.png:240 240
(略)
target/lena_75.png:240 240
target/lena_60.png:240 240
target/lena_48.png:240 240

real    0m2.957s
user    0m13.097s
sys 0m3.740s

実行時間(実時間)に関する結果をまとめると以下のようになる。parallelを利用することで直列に実行するよりも明らかに高速になった。

  • 直列実行:約9.8秒
  • parallelによる並列実行:約3.0秒

なお上記の実行環境はApple M2搭載のMacBook Air(8コア)である。

所感

業務においてシェルスクリプトを作成している中で、並列実行を簡単に実現するためのフレームワークを思いつき実装しようとしたが、parallelが存在することがわかり、まさに自分が実現したいような機能をもっていたので、ありがたく使わせていただいている。parallelを利用している中で気づいたことは、自分が作成するスクリプトにおいても、そのインタフェースはやはりシンプルにしておくべきということだ。今回とりあげたスクリプトを例に言うと、is_match.pyはひとつの画像に対してひとつの結果を出すことに徹している。複数の画像に対して複数の結果を出すなど、そういったことは一切考えていない。このようなシンプルさを追求したスクリプトは(ランタイム時の多少の無駄はあれ)他のツールと組み合わせやすく、「他のソフトウェアを梃子として利用する(UNIX哲学を参照のこと)」可能性を高めてくれる。