プログラミング
再帰的な `make` と `-j`
Recursive `make` and `-j` (quuxplusone.github.io)
要約
この記事では、Makefileのレシピ内で`make`を再帰的に呼び出す際のGNU Makeの挙動について、バージョン3.81から4.3にかけての変化を解説しています。特に、`-j`オプション(並列実行)が子プロセスに引き継がれない問題と、その解決策としてレシピ行の先頭に`+`を付けるか、`make`を`$(MAKE)`に置き換える方法について説明しています。また、これらの方法でも解消されない警告や、バージョンによる挙動の違いについても詳細な表を用いて比較しています。
全文翻訳
再帰的なmakeと-j
今日、私は「再帰的」なmakeの呼び出し、つまりMakefileのレシピ自体がmakeを呼び出すケースについて調べました。公式ドキュメントは非常に優れていますが、私は様々なGNU Makeのバージョンを扱っているため、GNU Make 3.81から4.3にかけて挙動がどのように変化したかを調べました。
基本的に、Makefileに次のようなレシピがある場合の問題です。
ser: make -C subdir
そして `make -j4 ser` を実行すると、最近のGNU Makeのバージョンでは警告(jobserver unavailable: using -j1)が表示されます。そして、並列実行したいのに、ジョブサーバー(基本的に-jオプション)が子makeに転送されないため、並列実行が失われます。さらに、呼び出したmakeと同じmakeバイナリが必ずしも使用されるわけではなく、パスにあるmakeが実行されます。
レシピ行の先頭に`+`を付けるか、(それが不快にマジカルに感じるので)`make`を`$(MAKE)`に置き換えることで、これら3つの問題すべてを修正できます。詳細は記事の最後にあります。
ジョブサーバーが正しく転送されるようになったので、コマンドラインで指定した並列度と再帰的なレシピで指定した並列度がどのように相互作用するかを疑問に思いました。テストに使用したMakefileのペアは以下の通りです。
detab -E -4 - >Makefile <<EOF
ser:
make -C subdir
par2:
make -j2 -C subdir
par4:
make -j4 -C subdir
par:
make -j -C subdir
serplus:
+make -C subdir
par2plus:
+make -j2 -C subdir
par4plus:
+make -j4 -C subdir
parplus:
+make -j -C subdir
sermake:
$(MAKE) -C subdir
par2make:
$(MAKE) -j2 -C subdir
par4make:
$(MAKE) -j4 -C subdir
parmake:
$(MAKE) -j -C subdir
EOF
mkdir subdir
detab -E -4 - >subdir/Makefile <<EOF
all: one two three four five
one:
echo 'one' && sleep 1 && echo 'ONE'
two:
echo 'two' && sleep 1 && echo 'TWO'
three:
echo 'thr' && sleep 1 && echo 'THR'
four:
echo 'fou' && sleep 1 && echo 'FOU'
five:
echo 'fiv' && sleep 1 && echo 'FIV'
EOF
その後、利用可能な様々なバージョンのMakeで `make ser`, `make -j2 ser` などをテストしました。以下はGNU Make 3.81(OS X上)の結果です。各セルの数字は並列実行されたレシピの数を示します。ダガー†が付いている4行は、テストしたMakeのバージョン間で異なります。これらのイディオムは「移植性がない」ため、実際には避けるべきだと私は思います。
make… -j2 -j4 -j
ser† 1 2 4 ∞
serplus 1 2 4 ∞
sermake 1 2 4 ∞
par2 2 2b 2b 2
par2plus 2 2b 2b 2
par2make 2 2b 2b 2
par4 4 4b 4b 4
par4plus 4 4b 4b 4
par4make 4 4b 4b 4
par† ∞ 2 4 ∞
parplus† ∞ 2 4 ∞
parmake† ∞ 2 4 ∞
上記の表で、「b」は次の警告を示します。
$ make -j2 par2
make[1]: warning: -jN forced in submake: disabling jobserver mode.
GNU Make 4.0(Debian 8.11上):
make… -j2 -j4 -j
ser† 1 1a 1a ∞
serplus 1 2 4 ∞
sermake 1 2 4 ∞
par2 2 2b 2b 2
par2plus 2 2b 2b 2
par2make 2 2b 2b 2
par4 4 4b 4b 4
par4plus 4 4b 4b 4
par4make 4 4b 4b 4
par† ∞ 1a 1a ∞
parplus† ∞ 2 4 ∞
parmake† ∞ 2 4 ∞
上記の表で、「a」と「b」はそれぞれ次の2つの警告を示します。
$ make -j2 ser
make[1]: warning: jobserver unavailable: using -j1. Add '+' to parent make rule.
$ make -j2 par2
make[1]: warning: -jN forced in submake: disabling jobserver mode.
GNU Make 4.2.1(Rocky 8.10上)およびGNU Make 4.3(Red Hat 9またはUbuntu 22.04上)では、後者のエラーメッセージが調整され、Nの値が含まれるようになります。
$ make -j2 par
make[1]: warning: -j0 forced in submake: resetting jobserver mode.
$ make -j2 par2
make[1]: warning: -j2 forced in submake: resetting jobserver mode.
また、行 `par`, `parplus`, `parmake` の挙動も変更されます。
make… -j2 -j4 -j
ser† 1 1a 1a ∞
serplus 1 2 4 ∞
sermake 1 2 4 ∞
par2 2 2b 2b 2
par2plus 2 2b 2b 2
par2make 2 2b 2b 2
par4 4 4b 4b 4
par4plus 4 4b 4b 4
par4make 4 4b 4b 4
par† ∞ ∞b ∞b ∞
parplus† ∞ ∞b ∞b ∞
parmake† ∞ ∞b ∞b ∞
「親makeルールに'+'を追加してください」と明示的に指示する警告「a」は、レシピ行の先頭に`+`を付けることで、常に消えます。例えば、次のようになります。
serplus:
+make -C subdir
この解決策は、`@`や`-`のプレフィックスと同様に、私には不快にマジカルに見えます。(個人的には、レシピ行の最後に「|| true」がある方が、先頭にマジカルな「-」があるよりも良いと思います。)
よりマジカルに見えない(しかし、内部的には同様にマジカルであるらしい)解決策は、`$(MAKE)`を使用することです。例えば次のようになります。
sermake:
$(MAKE) -C subdir
どちらの方法も、警告「b」(「ジョブサーバーモードのリセット」について)を沈黙させるわけではありません。
実際には、次のファンタジーのような行が欲しいところです。
make… -j2 -j4 -j
fantasy nprocs 2 4 ∞
しかし、現時点では、引用された行のいずれかから好みのトレードオフを選択する必要があります。
sermake 1 2 4 ∞
par4make 4 4b 4b 4
投稿日: 2026-09-30
カテゴリ: concurrency, makefiles, sre