そーだいなるらくがき帳

そーだいが自由気侭に更新します。

makeで簡単に処理を並列化する

 VOYAGE GROUP でお手伝いしているとmakeに詳しくなる。

 嘘、やっぱなにもわからん。

 アイスブレイクはこれくらいにしておいて、本題に入る。 同じ処理を繰り返し、実行したいことは多々ある。 更にその処理を並列にしたいことも多々ある。 そんなとき、makeが便利なので使い方を紹介する。

やりたいこと

 例えば次のような処理。

  1. CSVファイルを読み込む
  2. INSERT文に変換してDBに取り込む

 CSVファイルは経理データだったり、アクセスログだったりで、この処理を書くことは日常的にあると思う。 この場合にCSVファイルが例えば日付別にあって、一度で取り込みたい場合にどうするだろう?

 一番シンプルなのはファイル名の一覧を取得し、その一覧をもとにループで順番に、上記の処理を繰り返していくことだろう。 この場合にファイル数が多ければ多くなるほど基本的には指数関数的に処理時間が伸びていく。

 上記のようにINSERTするだけであれば各処理はコンフリクトしないので、次に目指すのは並列化だろう。 やり方は色々あるが、今ある処理をできるだけ活かしながら並列処理したい場合に、makeが強い味方になってくれる。 そのためのサンプルをご紹介する。

実際のやり方

 次のような hoge.sh があったとする。 このシェルスクリプトは引数をechoしているだけだが1回に3秒必ずかかる。 つまり、10回やると30秒かかるし、100回やると300秒かかる。 1000回、10000回などは到底待ってられない。

    #!/bin/sh
    sleep 3
    echo $@

 これを並列処理化し、例えば1000回を1000並列で処理できれば3秒程度*1で終わらすことができる。 このような処理を実現したい場合に書く。

ファイルの中身を並列実行

 次のような処理を想定している。

  1. list.txtに処理したい条件が列挙されている
  2. それをhoge.shに渡して並列処理したい。
$ cat list.txt
aaa
bbb
ccc
ddd

 このように処理したい内容が list.txt に記載されている。 次に実際のMakefileがこちら。

FILE := list.txt
LIST := $(shell cat $(FILE))
TARGETS := $(addsuffix .run,$(LIST))

all: $(TARGETS)
%.run:
    sh hoge.sh $* 2>&1 > $@.tmp
    mv $@.tmp $@

 makeの仕組み自体の細かい説明は割愛して、大まかに処理の流れを説明する。

 list.txtの中身を取り出し、 aaa.run のように置き換えてTARGETに指定している。 その後、実際に %.run に該当するので aaa.run が処理され、 hoge.sh $* が実行される。 その処理中は aaa.run.tmp に標準出力と標準エラー出力が出力され、完了すると aaa.run という成果物にリネームされる。

 そして重要なことはmakeは -j のオプションを使うと並列実行することができる。 例の -j 5 は5並列を表し、数字を指定しない場合は良しなに並列数が増えていく。 メモリが潤沢にある場合などは -j でも良いが、処理が重い場合などは並列数は抑えめに指定すること。

 そしてこの処理の実行結果がこちら。

$ /usr/bin/time make -j 5
sh hoge.sh aaa 2>&1 > aaa.run.tmp
sh hoge.sh bbb 2>&1 > bbb.run.tmp
sh hoge.sh ccc 2>&1 > ccc.run.tmp
sh hoge.sh ddd 2>&1 > ddd.run.tmp
mv aaa.run.tmp aaa.run
mv bbb.run.tmp bbb.run
mv ccc.run.tmp ccc.run
mv ddd.run.tmp ddd.run

0.01 user 0.00 system 0:03.01 elapsed 0% CPU (0avgtext+0avgdata 2868maxresident)k
8inputs+32outputs (1major+1671minor)pagefaults 0swaps


$ ls -la
total 36
drwxrwxr-x  2 ubuntu ubuntu 4096 Dec 22 09:03 ./
drwxrwxrwt 12 root   root   4096 Dec 22 08:58 ../
-rw-rw-r--  1 ubuntu ubuntu  149 Dec 22 08:57 Makefile
-rw-rw-r--  1 ubuntu ubuntu    4 Dec 22 09:03 aaa.run
-rw-rw-r--  1 ubuntu ubuntu    4 Dec 22 09:03 bbb.run
-rw-rw-r--  1 ubuntu ubuntu    4 Dec 22 09:03 ccc.run
-rw-rw-r--  1 ubuntu ubuntu    4 Dec 22 09:03 ddd.run
-rw-rw-r--  1 ubuntu ubuntu   26 Dec 22 08:57 hoge.sh
-rw-rw-r--  1 ubuntu ubuntu   16 Dec 22 08:57 list.txt

$ cat ccc.run
ccc

 ほぼ3秒で実行が終わり、並列処理されていることがわかる。 またこのように実行結果もしっかりとわかる。 この方式を教えてくれたのは @cynipe さんなので「さいないぷ方式」と名付ける。

フォルダの中身を引数で並列実行

 前述の例はとても汎用性が高いやり方で、DBからデータ一覧を取り出した一覧に対して実行するような場合、例えばユーザ一一覧に対してUPDATE文を流すようなケースではとても有効だ。

 しかし冒頭のようにフォルダにFILEがある場合に ls -1R target_path > list.txt のようなことをしなければならない。 この手間を減らすため、応用編も紹介する。

 次のようにlistフォルダの配下に処理したいファイルが存在するとする。

$ ll list
total 8
drwxrwxr-x 2 ubuntu ubuntu 4096 Dec 22 09:13 ./
drwxrwxr-x 3 ubuntu ubuntu 4096 Dec 22 09:13 ../
-rw-rw-r-- 1 ubuntu ubuntu    0 Dec 22 09:12 aaa
-rw-rw-r-- 1 ubuntu ubuntu    0 Dec 22 09:12 bbb
-rw-rw-r-- 1 ubuntu ubuntu    0 Dec 22 09:12 ccc
-rw-rw-r-- 1 ubuntu ubuntu    0 Dec 22 09:12 ddd

 このような場合は find コマンドを利用した下記のようなMakefileを用意する。

BASE_PATH := list
LIST := $(shell find $(BASE_PATH) -type f | grep -v '.run')
TARGETS := $(addsuffix .run,$(LIST))

all: $(TARGETS)
%.run:
    sh hoge.sh $* 2>&1 > $@.tmp
    mv $@.tmp $@

 LISTの作り方が違うだけで処理自体は一緒である。 実行結果は以下の通り、ファイルから実行対象を取り出したときと同様のように並列処理できる。

$ /usr/bin/time make -j 5
sh hoge.sh list/aaa 2>&1 > list/aaa.run.tmp
sh hoge.sh list/ccc 2>&1 > list/ccc.run.tmp
sh hoge.sh list/ddd 2>&1 > list/ddd.run.tmp
sh hoge.sh list/bbb 2>&1 > list/bbb.run.tmp
mv list/ccc.run.tmp list/ccc.run
mv list/bbb.run.tmp list/bbb.run
mv list/aaa.run.tmp list/aaa.run
mv list/ddd.run.tmp list/ddd.run
0.01 user 0.00 system 0:03.01 elapsed 0%CPU (0avgtext+0avgdata 3216maxresident)k
32inputs+32outputs (1major+1711minor)pagefaults 0swaps

ubuntu@ip-172-25-1-129:/tmp/work$ ls -la list
total 24
drwxrwxr-x 2 ubuntu ubuntu 4096 Dec 22 09:23 .
drwxrwxr-x 3 ubuntu ubuntu 4096 Dec 22 09:22 ..
-rw-rw-r-- 1 ubuntu ubuntu    0 Dec 22 09:12 aaa
-rw-rw-r-- 1 ubuntu ubuntu    9 Dec 22 09:23 aaa.run
-rw-rw-r-- 1 ubuntu ubuntu    0 Dec 22 09:12 bbb
-rw-rw-r-- 1 ubuntu ubuntu    9 Dec 22 09:23 bbb.run
-rw-rw-r-- 1 ubuntu ubuntu    0 Dec 22 09:12 ccc
-rw-rw-r-- 1 ubuntu ubuntu    9 Dec 22 09:23 ccc.run
-rw-rw-r-- 1 ubuntu ubuntu    0 Dec 22 09:12 ddd
-rw-rw-r-- 1 ubuntu ubuntu    9 Dec 22 09:23 ddd.run

 このように当初の目的を達成することができた。 このMakefileを応用すればアクセスログの集計などを並列処理にすることができる。

 この方法は一緒にやってる若者のsyotaと一緒にペアプロしながら書いたので「しょーた方式」と名付ける。

makeは一度処理した成果物を再実行しない

 makeで行う大きなもう一つの理由に「完了した処理を再実行しない」ようにコントールすることが簡単であることだ。 この例の場合、 aaa.run のような成果物を生成されているため再実行しても aaa は実行されない。 これはmakeがビルドツールであるがゆえの特徴*2でもあるし、並列処理を行う上でとても重要な要素である。

 つまり、中断しても再度やり直すことができるということだ。

$ ls -la list
total 8
drwxrwxr-x 2 ubuntu ubuntu 4096 Dec 22 09:32 ./
drwxrwxr-x 3 ubuntu ubuntu 4096 Dec 22 09:22 ../
-rw-rw-r-- 1 ubuntu ubuntu    0 Dec 22 09:12 aaa
-rw-rw-r-- 1 ubuntu ubuntu    0 Dec 22 09:29 bbb
-rw-rw-r-- 1 ubuntu ubuntu    0 Dec 22 09:12 ccc
-rw-rw-r-- 1 ubuntu ubuntu    0 Dec 22 09:12 ddd

$ make
sh hoge.sh list/aaa 2>&1 > list/aaa.run.tmp
mv list/aaa.run.tmp list/aaa.run
sh hoge.sh list/ccc 2>&1 > list/ccc.run.tmp
mv list/ccc.run.tmp list/ccc.run
sh hoge.sh list/ddd 2>&1 > list/ddd.run.tmp
mv list/ddd.run.tmp list/ddd.run
sh hoge.sh list/bbb 2>&1 > list/bbb.run.tmp
mv list/bbb.run.tmp list/bbb.run

$ ls -ls list
total 24
drwxrwxr-x 2 ubuntu ubuntu 4096 Dec 22 09:32 ./
drwxrwxr-x 3 ubuntu ubuntu 4096 Dec 22 09:32 ../
-rw-rw-r-- 1 ubuntu ubuntu    0 Dec 22 09:12 aaa
-rw-rw-r-- 1 ubuntu ubuntu    9 Dec 22 09:32 aaa.run
-rw-rw-r-- 1 ubuntu ubuntu    0 Dec 22 09:29 bbb
-rw-rw-r-- 1 ubuntu ubuntu    9 Dec 22 09:32 bbb.run
-rw-rw-r-- 1 ubuntu ubuntu    0 Dec 22 09:12 ccc
-rw-rw-r-- 1 ubuntu ubuntu    9 Dec 22 09:32 ccc.run
-rw-rw-r-- 1 ubuntu ubuntu    0 Dec 22 09:12 ddd
-rw-rw-r-- 1 ubuntu ubuntu    9 Dec 22 09:32 ddd.run

# ccc.run(cccの成果物を削除する)
$ rm list/ccc.run
# cccのみ実行される
$ make
sh hoge.sh list/ccc 2>&1 > list/ccc.run.tmp
mv list/ccc.run.tmp list/ccc.run

 このように差分だけ簡単に処理することができるのもmakeで行う大きなメリットである。

まとめ

 makeをビルド使うことはもちろん、コマンドを纏めたり、今回のように既存の処理を簡単に並列処理にすることにすることができる。 WindowsでもWSLがあるので普段の処理でも利用することができるし、様々なところで活用できる。

 ただし、make本体のデバッグはechoデバッグになりがちだし、テストなどは当然無いのでやりすぎは注意だ。

*1:厳密には3秒以上かかるだろうけど

*2:元々そういうものだし