CMakeの最新バージョン、3.16でtarget_precompile_header
がサポートされていた。ドキュメントは以下の通り。
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.cpp
をpch
ライブラリとしてコンパイルする。このときに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.cpp
とmain.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.gch
とpch2.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
で指定したファイルだけ先にインクルードしたファイルを作って、そのまとめたヘッダファイルをコンパイルしておくという形らしい。
この性質上、ヘッダを一つ変更すると結局全部コンパイルしなおしになる。これは実質的に実装をヘッダに書くinline
やtemplate
と相性が悪い。テンプレートを使わずヘッダには宣言しか書かない大規模プロジェクトでは速度向上が見込めるだろう。
個人的にはヘッダファイルごとにコンパイルしたいのだが、それができるかどうかもう少し調べないといけないようだ……。