CMakeでpchとccacheを併用する

コンパイラはGCCを前提としています。


## 設定

pch及びccache用の設定をCMakeに追加してあげればOK。
今回は設定するターゲット名を `app` としています。


### pch

ターゲットに対して `target_precompile_headers()` でpch化してあげたいヘッダファイルを設定してあげればOKです。
これで自動的にpchを作成してくれます。

```cmake
target_precompile_headers( app PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src/app.hpp" )
```


### ccache

2つの設定を行います。

* `target_compile_options()` で設定できるコンパイラオプションの設定[^1]
* `set_property()` の `RULE_LAUNCH_COMPILE` で設定できるコンパイル実行時のコマンドラインのプレフィックス設定[^2]

```cmake
find_program(CCACHE_FOUND ccache)
if(CCACHE_FOUND)
  target_compile_options( app PRIVATE -fpch-preprocess )
  set_property(
    TARGET app
    PROPERTY RULE_LAUNCH_COMPILE "CCACHE_SLOPPINESS=pch_defines,time_macros ccache"
  )
endif()
```

[^1]: https://ccache.dev/manual/4.7.3.html#_precompiled_headers
add the compiler option `-fpch-preprocess` when compiling.

[^2]: https://ccache.dev/manual/4.7.3.html#_precompiled_headers
You must set sloppiness to pch_defines,time_macros.



## 効果測定

実際どのくらいの効果がでるのか測定してみましょう。

### 測定対象

今回は[yaml-cpp](https://github.com/jbeder/yaml-cpp/tree/yaml-cpp-0.7.0)のビルドを測定します。

```bash
$ git clone git@github.com:jbeder/yaml-cpp.git -b yaml-cpp-0.7.0
$ cd ./yaml-cpp/
$ CC=gcc-10 CXX=g++-10 cmake -S . -B ./build/ -G Ninja

# ここで出た値を結果とする
$ time cmake --build ./build/ -- -j16
```

また、前述の設定を参考にプロジェクトの[CMakeLists.txt](https://github.com/jbeder/yaml-cpp/blob/yaml-cpp-0.7.0/CMakeLists.txt)に以下のような追記を行いました。今回は `yaml.h` のみpch化する設定を行っています。

```diff
diff --git a/CMakeLists.txt b/CMakeLists.txt
index b230b9e..4b67785 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -71,6 +71,17 @@ set(backport-msvc-runtime $<VERSION_LESS:${CMAKE_VERSION},3.15>)
 add_library(yaml-cpp ${yaml-cpp-type} "")
 add_library(yaml-cpp::yaml-cpp ALIAS yaml-cpp)
 
+find_program(CCACHE_FOUND ccache)
+if(CCACHE_FOUND)
+  add_compile_options( -fpch-preprocess )
+  set_property(
+    GLOBAL
+    PROPERTY RULE_LAUNCH_COMPILE "CCACHE_SLOPPINESS=pch_defines,time_macros ccache"
+  )
+endif()
+
+target_precompile_headers( yaml-cpp PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include/yaml-cpp/yaml.h" )
+
 set_property(TARGET yaml-cpp
   PROPERTY
     MSVC_RUNTIME_LIBRARY ${CMAKE_MSVC_RUNTIME_LIBRARY})
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 5ebc1a6..8b547f9 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -40,6 +40,8 @@ target_link_libraries(yaml-cpp-tests
     yaml-cpp
     gmock)
 
+ target_precompile_headers( yaml-cpp-tests REUSE_FROM yaml-cpp )
+
 set_property(TARGET yaml-cpp-tests PROPERTY CXX_STANDARD_REQUIRED ON)
 if (NOT DEFINED CMAKE_CXX_STANDARD)
   set_target_properties(yaml-cpp-tests PROPERTIES CXX_STANDARD 11)
```



### 条件

今回はいくつかの条件で測定を行い、それぞれの効果を計ってみようと思います。
条件は以下の通りです。

* `(a)` 何も設定しない場合
* `(b)` pchを利用した場合
* `(c)` ccacheを利用した場合（1回目）
* `(c')` ccacheを利用した場合（2回目）
* `(bc)` pchとccacheを併用した場合（1回目）
* `(bc')` pchとccacheを併用した場合（2回目）

これで測定の準備ができました。
さっそく測定してみましょう。


## 結果

以下のようになりました。
aとの比率は小数第1位までを有効とし、四捨五入で丸めています。

<!--
target | real ( user ) | aとの比率
--- | --- | ---
a | 0m9.164s ( 0m37.651s ) | 100.0% ( 100.0% )
b | 0m9.057s ( 0m30.827s ) | 98.8% ( 81.9% )
c | 0m13.369s ( 0m36.672s ) | 145.9% ( 97.4% )
c' | 0m0.824s ( 0m0.761s ) | 9.0% ( 2.0% )
bc | 0m11.493s ( 0m32.982s ) | 125.4% ( 87.6% )
bc' | 0m0.996s ( 0m3.927s ) | 10.9% ( 10.4% )

![chart.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/425840/4ffa9a08-1680-c7bf-8af3-adc7930d3e65.png)
-->

target | real ( user ) | aとの比率 | Ninja実行時のエッジ数
--- | --- | --- | ---
a | 0m11.895s ( 0m38.431s ) | 100.0% ( 100.0% ) | 60
b | 0m14.316s ( 0m31.802s ) | 120.4% ( 82.75% ) | 64 
c | 1m27.864s ( 0m37.927s ) | 738.7% ( 98.7% ) | 60
c' | 0m4.563s ( 0m1.015s ) | 38.4% ( 2.6% ) | 60
bc | 1m30.098s ( 0m35.561s ) | 757.4% ( 92.5% ) | 64
bc' | 0m4.964s ( 0m1.870s ) | 41.7% ( 4.9% ) | 64

![chart.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/425840/58d5da88-a13f-7f1c-e6ea-c6eed79960dc.png)


## 考察

（今回のプロジェクトに限った話なのであまり意味がないかもしれませんが）

aとb、cとbcの結果から、pchを利用するとreal時間が若干伸び、user時間が若干短縮している傾向が読み取れます。数秒だし誤差かなとも思ったのですが、何回やっても同じような傾向だったため、pchを追加したことによる効果が出ていそうです。
real時間が伸びているのはエッジ数が `60` から `64` に増えたことが関係していそうです。user時間は望んでいた結果になっているため、もう少し大きいかつ同じヘッダを使いまわすプロジェクトでないと効果が出にくいのかもしれませんね。

cは思った以上に時間がかかっていますね。公式サイトのパフォーマンス結果[^3]から多くて125%くらいかなと想像していたのですが、なんと738.7%というとんでもない比率が出てしまいました。
実はこの比率、バージョンが `v4.5.1` 以下のccacheを利用すると、145%程度まで下がります。`v4.6.3` で急激に上がることを確認したので、このバージョンまでの間に何かがあったのかもしれません。（デバッグした結果、iiファイルを書き出す部分で時間が跳ね上がっているのを確認したのでここで何かあったのかも）
じゃあバージョンを上げずに古いものを利用すればいいのでは？と思われるかもしれませんが、実は古いバージョンだとpchを利用した際の挙動が不安定になってしまうのです。

[^3]: https://ccache.dev/performance.html

<details><summary>（具体的にはこんな感じでうまくキャッシュできない現象が発生してしまいます）</summary><div>

```bash
# 1回目実行後に確認した結果
$ ccache -s
Summary:
  Hits:               0 /   33 (0.00 %)
    Direct:           0 /   55 (0.00 %)
    Preprocessed:     0 /   33 (0.00 %)
  Misses:            33
    Direct:          55
    Preprocessed:    33
  Uncacheable:       22
Primary storage:
  Hits:               0 /   88 (0.00 %)
  Misses:            88
  Cache size (GB): 0.04 / 5.00 (0.70 %)

# 2回目実行後に確認した結果
$ ccache -s
Summary:
  Hits:              33 /   66 (50.00 %)
    Direct:          33 /  110 (30.00 %)
    Preprocessed:     0 /   33 (0.00 %)
  Misses:            33
    Direct:          77
    Preprocessed:    33
  Uncacheable:       44
Primary storage:
  Hits:              66 /  176 (37.50 %)
  Misses:           110
  Cache size (GB): 0.04 / 5.00 (0.70 %)
```

Uncacheable の項目から22回ほどうまくキャッシュできていないことがわかりますね。
`-v` オプションを付けて詳細を確認するとpchに関する情報がでてきます。

```
Uncacheable:
  Could not use precompiled header:   44
```

また、毎回22ずつ増えるわけではなく、27や17ずつ増える場合もありました。`ccache -C` でキャッシュを削除すると増加量が変化したので、キャッシュ周りが怪しそうです。

</div></details>

ちなみに、この記事を執筆した時点の最新のバージョン `v4.7.3` ではこの不安定な挙動が解消されていることを確認しました。

（少なくとも2022年11月現在は）pchを安定した環境で利用したい方は最新のバージョンを、特に利用しないよという方は動作が早い古いバージョンのccacheを使うのがいいかもしれません（前者に関してはそこまでしてpchとccacheを併用し続けるのははたして意味があるのかという感じがしますが。。）


## おまけ

### 作業環境

今回作業した環境はこんな感じ。

```bash
$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=22.04
DISTRIB_CODENAME=jammy
DISTRIB_DESCRIPTION="Ubuntu 22.04.1 LTS"

$ apt update -y \
    && apt upgrade -y \
    && apt install -y gcc-10 g++-10 ninja-build cmake

# v4.6.3 以下では libzstd-dev libhiredis-dev が必要なので注意
$ git clone git@github.com:ccache/ccache.git -b v4.7.3
$ CC=gcc-10 CXX=g++-10 cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -S ./ccache/ -B ./ccache/build/
$ cmake --build ./ccache/build/
$ ninja -C ./ccache/build/ install

$ g++-10 --version
g++-10 (Ubuntu 10.4.0-4ubuntu1~22.04) 10.4.0

$ cmake --version
cmake version 3.22.1
# version 3.16 より前は target_precompile_headers() が利用できないので注意
```

### ccacheの `disable` 設定の判定例

ccache の `disable` 設定が有効かどうかで処理を分けたい方向け。

```cmake
set( IS_ENABLE OFF )

find_program(CCACHE_PROGRAM ccache)
if( CCACHE_PROGRAM )
  execute_process(
    COMMAND ${CCACHE_PROGRAM} --get-config disable
    OUTPUT_VARIABLE CCACHE_DISABLE
    RESULT_VARIABLE CCACHE_IS_ERROR
  )
  if( ${CCACHE_IS_ERROR} EQUAL "0" )
    string(REPLACE "\n" "" CCACHE_DISABLE ${CCACHE_DISABLE})
    message(STATUS "CCACHE_DISABLE: ${CCACHE_DISABLE}")
    if( ${CCACHE_DISABLE} STREQUAL "false" )
      set( IS_ENABLE ON )
    endif()
  endif()
endif()
```

`IS_ENABLE` が `ON` の場合は `disable` 設定が無効（ `false` ）に設定されています。