Wednesday, December 2, 2020

komake: Make の -j オプションに潜む罠とその解決策

ビルドツールのダジャレの大家と言えば @shinh さんですが、それはさておき、皆さんは今でも Make を使ってビルドすることが多いと思います。かく言う私も、その一人。

最近は CPU のコア数も多いですから、当然 -j 16 とか、やりたいわけです。大きいプロジェクトになればなるほど、威力絶大ですね。

ですが、ここで問題がひとつ。大規模プロジェクトでは Makefile が別の Makefile を呼び出すような依存関係が良く見受けられます。この際、ターゲット間の依存関係で菱形が存在すると(例: ターゲット sub1 と sub2 が shared に依存)、make shared が make sub1 と make sub2 から同時に起動されることが起こりえます。CMake で生成した Makefile の場合も、ターゲット毎に make を起動しますね。

二重起動が発生すると、ほとんどの Makefile は、同時起動されることを想定していませんから、ビルドは失敗したり、成果物が壊れたりしてしまいます。

実際に、以下のような Makefile を書いてみると、make shared が同時に動いてしまうことを確認できます(sleep 1が2回表示されていることに注目)。

$ cat Makefile
all: proj
proj: sub1 sub2
	touch $@
sub1:
	$(MAKE) shared
	touch $@
sub2:
	$(MAKE) shared
	touch $@
shared:
	sleep 1
	touch $@
clean:
	rm -f proj sub1 sub2 shared

$ make -j2
/Applications/Xcode.app/Contents/Developer/usr/bin/make shared
/Applications/Xcode.app/Contents/Developer/usr/bin/make shared
sleep 1
sleep 1
touch shared
touch shared
touch sub1
touch sub2
touch proj

どうしたらいいでしょう。

先に書いたように、問題は同一ターゲットを引数にとる make が重複起動されてしまうところです。ならば、make の並走度を別途制限すればいいのではないでしょうか。ある make が終了するためには、その make が呼び出した make が全て終了している必要があることを加味すると、単純に、呼び出し階層毎の make の同時実行数を1に制限してしまえば、この問題が解決するはずです。

ジャジャーン!!! と言うわけで書いたのが make のラッパースクリプト komake です!

わずか41行のスクリプトにして、求められる機能を実現しています。komake を使って先の Makefile を実行すると、ほら、このとおり!!!

$ komake -j2
komake shared
komake shared
sleep 1
touch shared
touch sub1
make[1]: `shared' is up to date.
touch sub2
touch proj

komake shared は2回呼び出されていますが、実際のビルド作業である sleep 1 と touch shared の呼び出しが1回に減少したことがわかります。これは、1回目の komake shared が終了した後に2回目の komake shared が呼び出され、既に「shared」ファイルが存在したから、ビルド作業は何も走らなかったからです。

完璧ですね!! すばらしい!!!!

なお、注意点として、多重起動されロックを取ろうとしている komake も make のジョブスロットを消費します。ですので、komake を使う場合には、-j オプションに渡す引数を CPU コア数 + ロックにひっかかりそうな make の数(たとえば4)等にセットするとよいでしょう。

ちなみに、名前の komake は、「こまけー問題なおしてるな」というセルフツッコミに由来します。細かな問題なのですが、大規模プロジェクトにおいて切実な make の待ち時間問題を解決する、かゆいところに手の届くツールになっていると思います。

。。。。。とか書いてるうちに、同僚の @gfx が、問題となっていたプロジェクトのビルドツールを Ninja に移行する作業を終わらせてしまいました。完敗だ。これはKO負けですね。