2020年12月8日火曜日

Zapcc を使った複数プログラムの並列ビルドでハマった話

※この記事は C++ Advent Calendar 2020 8日目の記事です。

Zapcc とは

zapcc は clang ベースのキャッシュを利用した高速な C++ コンパイラーです。
zapcc はサーバープロセスのメモリ上にキャッシュを構築して高速化します。
ヘッダーファイルの解析結果や template のインスタンス化などがキャッシュされます。

プリコンパイルヘッダーを使うよりも生成されたコードもキャッシュされるので高速です。
類似のツールで ccache というキャッシュツールがありますが、こちらは翻訳単位ごとにキャッシュする仕組みなのでフルビルドのときはキャッシュ生成するだけでキャッシュの恩恵を受けられませんが、 zapcc の場合は同じヘッダーファイルを include してるファイルで、template インスタンス化などのキャッシュが利用されるためフルビルドでも高速になります。

また、zapcc サーバープロセスがいる間は別のプログラムのコンパイル時でもキャッシュ利用可能なので複数のプログラムをビルドする際も高速になります。

導入は簡単でビルド・インストール後、 CXX=zapcc++ とするだけで使えます。
自作の C++ テスティングフレームワーク iutest のテスト環境として zapcc インストール済みの Docker を用意してるので、よろしければそちらからでも試せます。
https://hub.docker.com/r/srzzumix/zapcc

ハマったこと

zapcc はビルドを高速に行うためにキャッシュをサーバーに蓄えます。
キャッシュはサーバープロセスが生きてるうちは有効です。
つまり、1つの実行ファイルをビルドするときだけでなく、その後に別プログラムをビルドする場合にもキャッシュが有効になります。
ライブラリの大量にあるテストプログラムをビルドするときには、このキャッシュがとても有効に働くと思います。

筆者が開発している iutest でも、複数のテストプログラムがあるのでビルド時間が短縮されるのを期待しましたが、結果は「テストの失敗」でした。

何が起きたか。

ビルドは特に問題なくできているように見えました、しかしながらテストは失敗している。
どうやらテストが失敗するのは、デフォルト設定ではなくテスト用に機能の有効化・無効化をしているテストでした。
iutest はコンフィグマクロを定義することで任意の機能を有効にしたり、無効にしたりするのですが、これが zapcc と相性最悪でした。。

どうやら、以下のようにとあるテストでコンフィグを変えても、キャッシュされたヘッダーファイルを利用してしまうため、#define が伝わっていないことがわかりました。

#define IUTEST_HAS_VARIADIC_TEMPLATES   0
#include "iutest.hpp"
  
対策

並列ビルドしているとキャッシュされたヘッダーを利用してしまうため、直列にしました。 -j4 とかせず -j1 を指定。
はい。もこの時点で Zapcc の恩恵を捨てています。いやむしろビルド時間的には悪化する場合も考えられます。。。

次に、キャッシュサーバーのプロセスが生きたままになっているので、1プログラム作る度に kill するようにしました。。。

Makefile はこんな感じになりました。

ifeq ($(CXX_NAME),zapcc++)
BUILD4ZAPCC=pkill zapcc++; sleep 1
endif

$(TARGETS1) : $(OUTDIR)/% : %.cpp $(IUTEST_HEADERS) $(MAKEFILE)
	$(BUILD4ZAPCC)
	$(CXX) $(IUTEST_INCLUDE) $(CXXFLAGS) -o $@ $< $(LDFLAGS)

なんか無理くりですが、 iutest はビルド速度改善を目的に zapcc を使っているわけじゃなく、zapcc コンパイラで不具合がないか検証するために使っているので、これでもまぁいいのです。。

最後に

zapcc はすごいけど万能ではなかった。



0 件のコメント:

コメントを投稿