Apache ArrowとかMroongaとかいくつかC++ベースのビルドに時間がかかるプロジェクトの開発をしている須藤です。ccacheを使うことでこれらのプロジェクトのCI時間を短くする方法を紹介します。
ビルドキャッシュ
高速化のよくある方法がキャッシュです。一度計算した結果を保存しておき使いまわすことでそもそも処理をせず速くするというアプローチです。
C++のビルドでもキャッシュを使うことができます。これを実現するためのプロダクトはいくつかありますがccacheが代表的なプロダクトです。(元祖?ビルドキャッシュ関連のプロダクトの歴史には詳しくないのでだれか知っている人がいたら教えて。)
他にもMozillaが開発しているsccache(リモートストレージにキャシュを保存して複数マシンでキャッシュを共有できる)や、Microsoft Visual C++用に開発されたclcacheなどがあります。
Microsoft Visual C++で使うならclcacheしか選択肢がない時期もありましたが、今はccacheやsccacheなど他のプロダクトもMicrosoft Visual C++をサポートしていたり、clcacheはメンテナンスされていなかったりしてclcacheの出番はなくなっています。
さらっとccacheもMicrosoft Visual C++をサポートしていると書きましたが、実はサポートしたのはわりと最近です。2022-02-27にリリースされたccache 4.6からサポートしています。
Added support for caching calls to Microsoft Visual C++ (MSVC) and clang-cl (MSVC compatibility for Clang). [contributed by Cristian Adam, Luboš Luňák, Orgad Shaneh and Joel Rosdahl]
それでは、ccacheを使ってCMakeを使っているプロダクトのMicrosoft Visual C++でのビルド結果をキャッシュする方法を紹介します。
CMakeとccache
ccacheはccache cc ...というように普通のコンパイラーのコマンドラインの前にccacheをつけて使います。(ccacheをccにシンボリックリンクして使う使い方もあります。)
CMakeはccacheのようにコンパイラーの前につけるタイプのツールをサポートするためにCMAKE_<LANG>_COMPILER_LAUNCHERというCMake変数を用意しています。<LANG>の部分にはCやCXXが入ります。C++のコンパイラーのコマンドラインの前にccacheをつける場合は-DCMAKE_CXX_COMPILER_LAUNCHER=ccacheというように使います。
ということで、次のようにすればCMakeを使っているプロダクトのMicrosoft Visual C++でのビルド結果をccacheでキャッシュして高速化できそうですよね。(PowerShellのコマンドライン。)
cmake `
-G "Visual Studio 17 2022" `
-A x64 `
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache `
...
しかし!これは動きません。なぜならCMAKE_<LANG>_COMPILER_LAUNCHERはVisual Studio系のジェネレーター(CMakeがどのビルドシステム用のファイルを生成するかを指定するやつ、みたいな感じ)では使えないからです。<LANG>_COMPILER_LAUNCHERプロパティーの説明にあるとおり、MakefileかNinjaでしか使えません。
ということは、こう書くのか、と思いますよね。
cmake `
-G Ninja `
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache `
...
しかし!これは動きません。なぜならVisual C++のコンパイラー(cl.exe)がどこにあるかわからないからです。
ではどうすればよいかというと、Visual C++のコンパイラーの場所を教えてあげればよいです。そのための便利バッチファイルをVisual Studioが提供しています。GitHub Actionsのwindows-2019 hosted runnerではC:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.batにあります。ということで、これを使ってこんな感じに書きます。バッチファイルを使うのでPowerShellではなくcmd.exeを使わないといけないことに注意してください。
call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x64
cmake ^
-G Ninja ^
-DCMAKE_BUILD_CXX_COMPILER_LAUNCHER=ccache ^
...
デバッグビルドとビルドキャッシュ
これでキャッシュが効くようになるのですが、この話はこれでは終わりません。実際は-DCMAKE_BUILD_TYPEにRelease(リリース用ビルド)やらDebug(デバッグ用ビルド)やらRelWithDebInfo(デバッグ情報付きリリース用ビルド)やらを指定してビルドします。そして、Debug/RelWithDebInfoを指定するとキャッシュが効きません。なんと。
CMakeは-DCMAKE_BUILD_TYPEにDebug/RelWithDebInfoを指定したときはデフォルトで/Ziオプション1を使います。/Ziはデバッグ情報を含んだPDBファイルをビルド対象のオブジェクトファイルとは別に生成するためのオプションです。そして、/Ziが指定されているとキャッシュが効きません。
詳細は以下の各プロダクトのissueやREADMEを読んでくださいなのですが、簡単にキャッシュできない理由を説明しておきます。/Ziを使うと複数のファイルで共有のPDBファイルを使います。複数のファイルが同じファイルを生成・変更すると各ファイル単位でビルド結果が確定しないので各ファイル単位でのビルド結果をキャッシュできないのです。/Fdオプションを使うことで各ファイルごとに別のPDBファイルを使うようにすることもでき、そうするとsccacheでは/Zi付きでもキャッシュできるようになります。が、ccacheはまだそこまで頑張っていません。
- https://github.com/ccache/ccache/issues/1040
- https://github.com/mozilla/sccache#usage の"To egnerate PDB files ..."あたり
- https://github.com/frerich/clcache/issues/30
では、どうするかというと/Ziの代わりに/Z7を使います。/Z7を使うと別途PDBファイルを作るのではなくオブジェクトファイルそのものにデバッグ情報を含めるようになります。そうすると、各ファイルのビルドでは他のファイルと成果物を共有しなくなるのでキャッシュできます。
CMakeLists.txtを変更できるなら、sccacheのREADMEにもある通り、次のようにして/Ziを/Z7に書き換えることになります。
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
string(REPLACE "/Zi" "/Z7" CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG}")
string(REPLACE "/Zi" "/Z7" CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG}")
elseif(CMAKE_BUILD_TYPE STREQUAL "Release")
string(REPLACE "/Zi" "/Z7" CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE}")
string(REPLACE "/Zi" "/Z7" CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE}")
elseif(CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")
string(REPLACE "/Zi" "/Z7" CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO}")
string(REPLACE "/Zi" "/Z7" CMAKE_C_FLAGS_RELWITHDEBINFO "${CMAKE_C_FLAGS_RELWITHDEBINFO}")
endif()
変更できないなら-DCMAKE_C_FLAGS_DEBUG=...とか-DCMAKE_CXX_FLAGS_DEBUG=...をいい感じに指定することになります。デフォルト値はCMakeのModules/Platform/Windows-MSVC.cmakeを見てください。
GitHub Actionsとビルドキャッシュ
ということでキャッシュできるようになったので、後はキャッシュ結果を各ビルド間で共有すればよいです。GitHub Actionsでキャッシュ結果を共有するにはactions/cacheを使います。
問題はなにをactions/cacheでキャッシュすればよいかというところです。キャッシュするのはccacheがキャッシュした内容です。それがどこにあるかはccache --show-configやccache --show-stats --verboseでわかります。たとえば、choco install ccacheでccacheをインストールした場合は~\AppData\Roaming\ccache\以下にキャッシュした内容が置かれます。
ということで、次のような設定を書けばよいということになります。
- uses: actions/cache@v3
with:
path: |
~\AppData\Roaming\ccache
key: ccache-${{ hashFiles('**/*.cpp', '**/*.hpp') }}
restore-keys: ccache-
具体例はMroongaの設定でも見てみてください。
まとめ
GitHub Actionsでccacheを使ってCMake+Microsoft Visual C++のビルドを高速化する方法をまとめました。
新しめのccacheを使わないといけない、CMAKE_CXX_COMPILER_LAUNCHERを使うならVisual Studio系のジェネレーターは使えない、-DCMAKE_BUILD_TYPE=Debug/-DCMAKE_BUILD_TYPE=RelWithDebInfoではキャッシュが効かないとかいろいろハマりポイントがありますが、動くようになるとかなり高速になるのでCIが遅いと思っている人は使ってみてください。