Makefileと並行taskと生成されたtaskの実行の悩み

はじめに

make に -j オプションを渡してあげると、良い感じにmakeが並行実行してくれる。

Makefile

default: do01 do02 do03

do01:
   @echo start 01
  sleep 1
   @echo end 01

do02:
   @echo start 02
  sleep 1
   @echo end 02

do03:
   @echo start 03
  sleep 1
   @echo end 03

-j を付けない場合。3秒掛かる。

$ time make
start 01
sleep 1
end 01
start 02
sleep 1
end 02
start 03
sleep 1
end 03

real    0m3.057s
user    0m0.011s
sys 0m0.029s

-j を付けた場合。1秒で終わる。

$ time make -j 4
start 01
sleep 1
start 02
sleep 1
start 03
sleep 1
end 02
end 03
end 01

real    0m1.027s
user    0m0.013s
sys 0m0.021s

-j をつければ、do01,do02,do03のtaskが並行して実行されるので1秒で済む。つまり複数のtaskを同時にやりたければ、そしてそれをmakeに任せるならtaskを定義しなくてはいけない。

並行実行できない処理(良くない)

当然だけれど。以下のような書き方では並行にtaskが実行できない。

内部でシェルのループが動いているだけ。

default:
  for i in `jot 3 1`; do echo start $$i; sleep 1; echo end $$i; done

同様にこれもダメ。

タスクが区切られていないのでシーケンシャルに実行されていくだけ。

default:
   @echo start 01
  sleep 1
   @echo end 01
   @echo start 02
  sleep 1
   @echo end 02
   @echo start 03
  sleep 1
   @echo end 03

並行実行できるにはできるけれど微妙なもの

(こういう書き方もあるけれど微妙)

run.bash

#!/bin/bash
echo $1
sleep 1
echo end $1
default:
  ./run.bash 01& ./run.bash 02& ./run.bash 03& wait

(ちなみに1行で書かないと上手くwaitが機能してくれない。それはそう)

なるべく並行taskの数はmake側で制御したい

なるべく並行taskの数はmake側で制御したい。というかmakeにおまかせしたい。

そうなると結局、個別の出来事毎にtaskを作らざる負えない。

taskの生成

makeをタスクランナー的に使っている場合に、複数のファイルに対して全部同じ処理を行いたい、ただし並行に実行されて欲しいということがある。このような時にtaskの生成が欲しくなる。

方法としては2つある。

makeの知識を必要としないという意味では前者が良いかもしれない。一方で無駄な中間ファイルなどができる。 makeで完結するという意味では後者が良いかもしれない。しかしちょっと読むのがむずかしいMakefileになってしまうような気がする。

それぞれのの仕方についてメモしておく。

Makefileを生成

こちらは何らかの方法を使ってMakefile自体を生成する方法。例えば、jinja2などのtemplate engineを使ってMakefile(run.mk)を生成する。 トップレベルのMakefileからは生成されたMakefile(run.mk)を呼び出すようにする

Makefile

default: run.mk
  $(MAKE) -f run.mk

clean:
  rm -f *.mk

run.mk:
  kamidana run.mk.j2 > run.mk

自分たちが慣れているtemplate engineの機能を使ってMakefileを作る分良いような気がしている。

run.mk.j2

do: {% for i in range(1, 4) %}do0{{i}} {% endfor %}

{% for i in range(1, 4) %}
do0{{i}}:
    @echo start 0{{i}}
    sleep 1
    @echo end 0{{i}}
{% endfor %}

以下のようなrun.mkが生成され利用される。

do: do01 do02 do03 
do01:
   @echo start 01
  sleep 1
   @echo end 01
do02:
   @echo start 02
  sleep 1
   @echo end 02
do03:
   @echo start 03
  sleep 1
   @echo end 03
$ time make -j 4
kamidana run.mk.j2 > run.mk
make -f run.mk
start 01
sleep 1
start 02
sleep 1
start 03
sleep 1
end 02
end 03
end 01

real    0m1.248s
user    0m0.191s
sys 0m0.057s

細かい話(taskを登録するかbuild対象のファイルを登録するか)

ただ、少し面倒くさい部分があるかもしれない。元々Makefileはbuild時の依存関係を記述するものなのでタスクランナーではない。

$ make run.mk
make: `run.mk' is up to date.

# (もちろん、 `make -B -j 4` など `-B` をつければ再度呼ばれる(always-make))
$ make -B run.mk
kamidana run.mk.j2 > run.mk

そんなわけで、先程の例の用に run.mk をbuild対象のファイルとして指定した場合に、既に run.mk が存在していれば生成されなくなる。

常に最新の値で何度でもbuildしたい場合には生成されるファイルをMakefileに記述しない方が良いかも知れない(例えば、genRunMakeというtask名などにし.PHONYを付加する)。

Makefileの中でtaskを生成

Makefile中でtaskを生成することもできる。プリプロセッサの様なイメージ。とは言え、こちらのMakefileMakefile読者中級以上の知識を必要とするような気がする(中級という級位に特に意味はないけれど)。

これには、foreach,eval,make,shellを使う*1

Makefile

NUMS = $(shell jot 3 1 | sed 's/^/0/g')

default: $(foreach i,${NUMS},do$i)

define _genTask
do$1:
   @echo start $1
   sleep 1
   @echo end $1
endef

$(foreach i,${NUMS},$(eval $(call _genTask,$i)))

短い事は短いのだけれど、callで文字列置換的にタスクを生成し、それをevalで解釈するというループ(foreach)を記述するということが理解できるかどうか。読める人は読めるのだけれど。ちょっと解釈が難しい記述になってしまう。

もちろん、並行で動作する。

$ time make -j 4
start 01
start 02
sleep 1
sleep 1
start 03
sleep 1
end 02
end 03
end 01

real    0m1.070s
user    0m0.023s
sys 0m0.056s

結論はまだ出ていない

結論はまだ出ていない。あんまりmakeに深い入りするのもどうかと思う一方で生成のフローが煩雑になるというのも良くない。ただ並行数の制御はmakeに任せたい。

*1:substやwildcardなども使うかもしれない