CMake 3.16でprecompiled headerがサポートされていた

CMakeの最新バージョン、3.16でtarget_precompile_headerがサポートされていた。ドキュメントは以下の通り。

cmake.org

precompiled headerは、大規模なプロジェクトなどで変更がないのに様々な.cppファイルから何回もインクルードされるヘッダーファイルを先に(できる分だけ)コンパイルしておいてビルドを高速化する技術だ。大抵のヘッダファイルには実装がないのでコード出力は必要ないが、パースして構文木を作ったりするだけでも時間がかかるので、そこを省略するだけでも結構違うのだろう。

ドキュメントを見たところ、これは以下のような形で使うようだ。

target_precompile_headers(main
  PUBLIC project_header.h
  PRIVATE <unordered_map>
)

どういう形で動くのかを少し調べてみた。以下のような構成になっているとしよう。名前は「pre compiled header」でpchにした。

$ tree .
.
├── CMakeLists.txt
├── pch
│   ├── CMakeLists.txt
│   ├── pch.cpp
│   └── pch.hpp
└── src
    ├── CMakeLists.txt
    └── main.cpp

2 directories, 6 files

中身は生命・宇宙全ての答えを計算するプログラムを書いた。これに関しては私が答えを知っているので大幅な高速化ができて、コードは以下のようになる。

namespace pch
{

int foo()
{
    return 42;
}

} // pch
#ifndef PCH_PCH_HPP
#define PCH_PCH_HPP

namespace pch
{

int foo();

} // pch
#endif// PCH_PCH_HPP

これでこの機能を試してみよう。pch/pch.cpppchライブラリとしてコンパイルする。このときにtarget_precompile_headersを足しておく。

# pch/pch.hpp
add_library(pch pch.cpp)

target_precompile_headers(pch
    PRIVATE ${PROJECT_SOURCE_DIR}/pch/pch.hpp)

ついでにsrc/main.hppにも足しておこう。

# src/CMakeLists.txt
add_executable(main main.cpp)
target_include_directories(main PRIVATE ${PROJECT_SOURCE_DIR})
target_link_libraries(main pch)

target_precompile_headers(main
    PRIVATE ${PROJECT_SOURCE_DIR}/pch/pch.hpp)

cmake-3.16をダウンロードしてコンパイルしてみる。

$ make
Scanning dependencies of target pch
[ 16%] Building CXX object pch/CMakeFiles/pch.dir/cmake_pch.hxx.gch
[ 33%] Building CXX object pch/CMakeFiles/pch.dir/pch.cpp.o
[ 50%] Linking CXX static library libpch.a
[ 50%] Built target pch
Scanning dependencies of target main
[ 66%] Building CXX object src/CMakeFiles/main.dir/cmake_pch.hxx.gch
[ 83%] Building CXX object src/CMakeFiles/main.dir/main.cpp.o
[100%] Linking CXX executable main
[100%] Built target main

どうやらcmake_pch.hxx.gchが追加されているようだ。だがpch.cppmain.cppの2回コンパイルされている。勝手にシェアはされないらしい。

シェアさせてみよう。REUSE_FROMというのがあって、ターゲット間でプリコンパイルヘッダを共有できるそうだ。pchライブラリからはmainが見えないので、mainからpchのヘッダを再利用する。

add_executable(main main.cpp)
target_include_directories(main PRIVATE ${PROJECT_SOURCE_DIR})
target_link_libraries(main pch)

# target_precompile_headers(main
#     PRIVATE ${PROJECT_SOURCE_DIR}/pch/pch.hpp)
target_precompile_headers(main REUSE_FROM pch)

すると以下のように、cmake_pch.hxx.gch`は一回しかコンパイルされなくなる。

$ make
Scanning dependencies of target pch
[ 20%] Building CXX object pch/CMakeFiles/pch.dir/cmake_pch.hxx.gch
[ 40%] Building CXX object pch/CMakeFiles/pch.dir/pch.cpp.o
[ 60%] Linking CXX static library libpch.a
[ 60%] Built target pch
Scanning dependencies of target main
[ 80%] Building CXX object src/CMakeFiles/main.dir/main.cpp.o
[100%] Linking CXX executable main
[100%] Built target main

シンプルな場合の挙動はだいたいわかった。ではより現実に近い状況として、ヘッダを複数個用意してみよう。

「生命・宇宙・全ての答え」の問いの定式化は非常に難しく諸説存在するが、その一つを実際に計算してみるコードを書いてみる。

namespace pch
{

int bar()
{
    return 6*9;
}

} // pch
#ifndef PCH_PCH2_HPP
#define PCH_PCH2_HPP

namespace pch
{

int bar();

} // pch
#endif// PCH_PCH2_HPP

これをpchディレクトリにpch2.[hc]ppという名前で置く。そしてCMakeLists.txtを以下のように書く。

add_library(pch pch.cpp pch2.cpp)

target_precompile_headers(pch
    PRIVATE ${PROJECT_SOURCE_DIR}/pch/pch.hpp
    PRIVATE ${PROJECT_SOURCE_DIR}/pch/pch2.hpp)

ではmakeしてみよう。予想としてはpch.hpp.gchpch2.hpp.gchをビルドすると思うところだが……?

$ make
Scanning dependencies of target pch
[ 16%] Building CXX object pch/CMakeFiles/pch.dir/cmake_pch.hxx.gch
[ 33%] Building CXX object pch/CMakeFiles/pch.dir/pch.cpp.o
[ 50%] Building CXX object pch/CMakeFiles/pch.dir/pch2.cpp.o
[ 66%] Linking CXX static library libpch.a
[ 66%] Built target pch
Scanning dependencies of target main
[ 83%] Building CXX object src/CMakeFiles/main.dir/main.cpp.o
[100%] Linking CXX executable main
[100%] Built target main

ヘッダが一つしかコンパイルされていない。どういうことだろう。

そもそも、CMakeはout-of-sourceを推奨しており、ヘッダファイルと同じディレクトリに生成された同名のsomething.hpp.gchを探すプリコンパイルヘッダの性質と最悪に相性が悪いはずだ。そもそも何がコンパイルされているのだろう。

というわけで見に行くことにした。ログを見ると、pch/CMakeFiles/pch.dir/cmake_pch.hxx.gchコンパイルされている。このディレクトリに実際にcmake_pch.hxxというファイルがあるので見てみよう。

/* generated by CMake */

#pragma GCC system_header
#ifdef __cplusplus
#include "/home/username/Sandbox/cmake/precompile_headers/pch/pch.hpp"
#include "/home/username/Sandbox/cmake/precompile_headers/pch/pch2.hpp"
#endif // __cplusplus

ディレクトリ構造見えてるやん。ユーザー名は置き換えました。

はい。で、これは単にtarget_precompile_headersに指定したファイルを全部インクルードしただけのファイルのようだ。だがこのファイルをインクルードした覚えはない(生成されたものなので当然)。どうやってコンパイルされているんだ。

make VERBOSE=1で実際に実行されているコマンドを見てみると、-include cmake_pch.hxxとある。このオプションを付けてファイルをコンパイルすると#include "cmake_pch.hpp"が書かれているのと同じように処理される。ドキュメントは以下。

Using the GNU Compiler Collection (GCC): Preprocessor Options

これによって依存関係を作り、target_precompile_headersで指定したファイルだけ先にインクルードしたファイルを作って、そのまとめたヘッダファイルをコンパイルしておくという形らしい。

この性質上、ヘッダを一つ変更すると結局全部コンパイルしなおしになる。これは実質的に実装をヘッダに書くinlinetemplateと相性が悪い。テンプレートを使わずヘッダには宣言しか書かない大規模プロジェクトでは速度向上が見込めるだろう。

個人的にはヘッダファイルごとにコンパイルしたいのだが、それができるかどうかもう少し調べないといけないようだ……。