VOYAGE GROUP でお手伝いしているとmakeに詳しくなる。
Makefileなにもわからん。
— そーだい@初代ALF (@soudai1025) 2020年12月22日
読むのはまだしも、スラスラ書けんな。
嘘、やっぱなにもわからん。
アイスブレイクはこれくらいにしておいて、本題に入る。 同じ処理を繰り返し、実行したいことは多々ある。 更にその処理を並列にしたいことも多々ある。 そんなとき、makeが便利なので使い方を紹介する。
やりたいこと
例えば次のような処理。
- CSVファイルを読み込む
- 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で終わらすことができる。 このような処理を実現したい場合に書く。
ファイルの中身を並列実行
次のような処理を想定している。
- list.txtに処理したい条件が列挙されている
- それを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があるので普段の処理でも利用することができるし、様々なところで活用できる。