ccacheを導入してUnityのiOSビルドを高速化出来ないか検証してみた

「[ccache](https://ccache.dev/)[^1]がUnityのiOSビルド[^2]でも利用できるのか？」「また、出来るとした場合にはどれくらい効果があるのか？」について調べてみました。

[^1]: 簡単に言えばC/C++に於けるビルド高速化ツール。ざっくりとした概要は[Wikipediaの記事](https://ja.wikipedia.org/wiki/Ccache)参照
[^2]: 正確に言うとUnityが出力した`xcodeproj`に対するXcodeビルド

私自身、`ccache`の導入そのものが初だったこともあるので、前半は主に**「`ccache`のインストール方法や設定項目」**などについて記載していき、後半から**「Unity上での導入手順及び効果」**について記載していければと思います。
(予め`ccache`を知っている方は前半部分読み飛ばしても良いかも)


もし間違いや不足などあればコメントにて教えて頂けると幸いです！ :bow: 


### ※まだWIP

書いておいてなんですが...後半の「Unity iOSビルドで導入した際の効果」についてはまだ未調査なところがチラホラと有り、完全に調べきれているとは言い切れないところもあるので、一先ずは**「雰囲気で導入してみた際の途中経過までのメモ書き」**的な感じで読んで頂ければと思います。。
(未調査項目は必要に駆られたタイミングで調査を再開し、随時追記していく予定..)



# TL;DR

先にUnity iOSビルドで導入した際の効果について、今の時点での検証結果から纏めます。
(導入関連は特に纏めることが無いので割愛)

※Unity iOSビルドで出力される一式を記事中では`xcodeproj`と表記してます。以降の解説でも同様の表記となります。

- **Unityから出力した`xcodeproj`にて、一度ビルドした後にCleanしてからリビルドしたらキャッシュヒットしてビルド時間が短くなった**
- **「Build」と「Archive」は別のキャッシュが作られる**
    - → 「Build」後 に「Archive」を行っても後者のビルド時間は短くならない
- **Unity iOSビルド時に「Append」で`xcodeproj`を更新してもキャッシュヒットは維持される**
- **Unity iOSビルド時に「Replace」で`xcodeproj`を置き換えたらキャッシュヒットしなくなった**
    - ※「既存のxcodeprojを手動で削除 → ビルド」も同様


結果だけで見ると「Appendでの更新」や「一度ビルドした後のClean/Build」ではキャッシュヒットして高速化出来るものの、**Replace相当の手順で`xcodeproj`そのものを置き換えてしまうとキャッシュヒットしなくなるように思われました。**


とはいえまだ雰囲気で使っているところはある上に、原因については詳細まで追いきれていないので、気が向いたら調査を再開していきたいところ...。
→ 幾つかの未調査項目などは[こちら](https://qiita.com/mao_/private/96795c1e734129430310#%E6%9C%AA%E8%AA%BF%E6%9F%BB%E9%A0%85%E7%9B%AE)に纏めている。








# ccacheの導入

macOS上でのccacheの導入方法や各種設定項目についてメモしていきます。
(導入時のOSは`macOS Catalina 10.15.3`)

## インストール

`brew`コマンドからインストールできます。

```shell
$ brew install ccache
```

## ccacheの設定について

幾つか必要そうな情報だけメモします。
詳細は`ccache -h`でヘルプを参照してください。

### 設定の確認

`ccache -p`を叩くことで現在の構成を確認できます。
各項目の概要については[ドキュメント](https://ccache.dev/manual/3.7.9.html)にある「Configuration settings」の章をご確認ください。

<details><summary>`ccache -p`の出力結果(クリックで展開)</summary><div>

出力結果のプレフィックスに`(default)`と付いている物はデフォルト設定であり、`(/Users/<user>/.ccache/ccache.conf)`と付いているものはユーザーが任意で設定変更した項目となります。
※以下で言うと`max_size = 5.0G`が変更されている。

```shell
# -p, --show-config         show current configuration options in
#                           human-readable format
$ ccache -p
(default) base_dir =
(default) cache_dir = /Users/<user>/.ccache
(default) cache_dir_levels = 2
(default) compiler =
(default) compiler_check = mtime
(default) compression = false
(default) compression_level = 6
(default) cpp_extension =
(default) debug = false
(default) depend_mode = false
(default) direct_mode = true
(default) disable = false
(default) extra_files_to_hash =
(default) hard_link = false
(default) hash_dir = true
(default) ignore_headers_in_manifest =
(default) keep_comments_cpp = false
(default) limit_multiple = 0.8
(default) log_file =
(default) max_files = 0
(/Users/<user>/.ccache/ccache.conf) max_size = 5.0G
(default) path =
(default) pch_external_checksum = false
(default) prefix_command =
(default) prefix_command_cpp =
(default) read_only = false
(default) read_only_direct = false
(default) recache = false
(default) run_second_cpp = true
(default) sloppiness =
(default) stats = true
(default) temporary_dir =
(default) umask =
```

</div></details>


### キャッシュサイズの変更

キャッシュサイズは`ccache -M`から変更できます。
単位は`G`、`M`など付ければ指定可能です。

```shell
# -M, --max-size=SIZE       set maximum size of cache to SIZE (use 0 for no
#                           limit); available suffixes: k, M, G, T (decimal)
#                           and Ki, Mi, Gi, Ti (binary); default suffix: G
$ ccache -M 1G
Set cache size limit to 1.0 GB
```

#### ※設定ファイルから変更する場合

コマンドなどで設定した情報は`ccache.conf`と言うファイルに保存されます。
こちらは幾つか存在しますが、今回は以下のパスにある設定ファイルに変更を加えてます。(詳細は「[ドキュメント](https://ccache.dev/manual/3.7.9.html) -> Configuration」を参照)

> `/Users/<user>/.ccache/ccache.conf`

こちらを直接編集することでも設定変更が可能です。

```conf:ccache.conf
max_size = 1G
```

#### ※環境変数から設定を変更する場合

設定ファイル以外にも環境変数からも設定を変更することが出来ます。

環境変数名は「[ドキュメント](https://ccache.dev/manual/3.7.9.html) -> Configuration settings」にある項目名横のカッコ内に記載されてます。(e.g. `max_size (CCACHE_MAXSIZE)`)

```shell
# 最大キャッシュサイズの変更
$ export CCACHE_MAXSIZE=5G
```


### キャッシュディレクトリの変更

キャッシュディレクトリの変更は以下のコマンドから変更できます。(外部ストレージを指定する例)
こちらも`ccache.conf`を書き換えたり、環境変数から指定して変更することも可能です。

```shell
# -o, --set-config=K=V      set configuration item K to value V
$ ccache -o cache_dir=/Volumes/<外部ストレージ>/ccache
```


**`ccache.conf`の書き換え**

```conf:ccache.conf
max_size = 5G
cache_dir = /Volumes/<外部ストレージ>/ccache
```

**環境変数から指定**

```shell
# 最大キャッシュサイズの変更
$ export CCACHE_DIR=/Volumes/<外部ストレージ>/ccache
```

##### ※ `-o`コマンドについて

`-o`コマンドは`Key(設定項目名)`と`Value(設定値)`を指定することで変更を適用できるコマンドです。
(イメージ的にはコマンドから`ccache.conf`を書き換えることが出来る機能)

ただし、中には`ccache -M`の様にデフォルトで設定変更がコマンド化されているものもあります。

```shell
// これら2つは同じ機能
$ ccache -o max_size=1G
$ ccache -M 1G
```



## キャッシュデータについて

### 統計情報の確認

`ccache -s`を叩くと**キャッシュの統計情報**を確認することができます。
各項目の概要については[ドキュメント](https://ccache.dev/manual/3.7.9.html)にある「Cache statistics」の章をご確認ください。

```shell
# -s, --show-stats          show summary of configuration and statistics
#                           counters in human-readable format
$ ccache -s
cache directory                     /Users/<user>/.ccache
primary config                      /Users/<user>/.ccache/ccache.conf
secondary config      (readonly)    /usr/local/Cellar/ccache/3.7.9/etc/ccache.conf
cache hit (direct)                     0
cache hit (preprocessed)               0
cache miss                             0
cache hit rate                      0.00 %
cleanups performed                     0
files in cache                         0
cache size                           0.0 kB
max cache size                       5.0 GB
```

### キャッシュの削除

キャッシュは`ccache -C`で削除できます。

```shell
# -C, --clear               clear the cache completely (except configuration)
$ ccache -C
Cleared cache
```

**※ちなみに、大文字の`-C`と小文字の`-c`は別コマンドになっているので注意。**

```shell
# -C, --clear               clear the cache completely (except configuration)
$ ccache -C

# -c, --cleanup             delete old files and recalculate size counters
#                           (normally not needed as this is done
#                           automatically)
$ ccache -c
```

### 統計情報のクリア

`ccache -s`で出力できる統計情報は`ccache -z`を叩くことで初期化する事が出来ます。

```shell
# -z, --zero-stats          zero statistics counters
$ ccache -z
Statistics zeroed
```










# UnityのiOSビルドで導入してみた

ここからはUnityのiOSビルドにて導入する際の手順や結果などについて纏めていきます。


## 導入手順

導入手順については以下の記事を参考にさせていただきました。
順に解説していきます。

・[ccacheを導入してC++のコンパイルを最適化](https://qiita.com/tani-shi/items/e1493e63a02966ef1bac)


ちなみに、iOSビルドでは主にUnityから出力された`xcodeproj`に対して`ccache`を適用し、**Xcodeビルドの高速化を行っていく形となります。**
**→ 逆に言うとUnity側のプロセス(`xcodeproj`の出力など)自体は`ccache`の適用外** と言う認識


### Xcode側でccacheを呼び出すためのラッパースクリプトを用意

`xctoolchain`内にccacheを呼び出すためのラッパースクリプトを用意します。
参考記事に倣ってshell内で完結させてますが、予め`ccache_wrapper`を別途生成して該当パスに保存 → パーミッションの変更で用意することも可能です。

```shell
$ sudo vi /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ccache_wrapper

----------------------------------
#!/bin/bash

which ccache
if [ "$?" -eq 0 ]; then
    # ※ `CCACHE_SLOPPINESS(sloppiness)`はccache.confで設定することも可能
    export CCACHE_SLOPPINESS=pch_defines,time_macros,clang_index_store
    exec `which ccache` /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang -Qunused-arguments "$@"
else
    exec /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang -Qunused-arguments "$@"
fi
----------------------------------

$ sudo chmod 755 /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ccache_wrapper
```

### Editor拡張で`xcodeproj`に対してラッパースクリプトを自動設定

後はUnity側で以下のEditor拡張を用意します。
**こちらを導入することで、iOSビルド時に必要な`User-Defined`等が自動で設定されます。**
(上記で用意した`ccache_wrapper`の設定など)

```csharp
#if UNITY_IOS
using System.IO;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEditor.iOS.Xcode;

namespace Samples
{
    static class CcacheSettings
    {
        /// <summary>
        /// xcodeprojにccache向けの設定を適用するサンプル
        /// </summary>
        [PostProcessBuild]
        public static void OnPostprocessBuild(BuildTarget buildTarget, string path)
        {
            if (buildTarget == BuildTarget.iOS)
            {
                var pbxProjectPath = PBXProject.GetPBXProjectPath(path);
                var pbxProject = new PBXProject();
                pbxProject.ReadFromString(File.ReadAllText(pbxProjectPath));

                var target = pbxProject.ProjectGuid();
                pbxProject.AddBuildProperty(target, "CC", "$(DT_TOOLCHAIN_DIR)/usr/bin/ccache_wrapper");

                File.WriteAllText(pbxProjectPath, pbxProject.WriteToString());
            }
        }
    }
}
#endif
```



## ビルド時間の計測


ビルドに用いる検証用プロジェクトには、[以前書いたこちらの記事](https://qiita.com/mao_/items/a5548f31de479c6831e4)にあるゲームプロジェクトを利用します。
(実案件レベルとは行かずとも、コンパイル時間を検証するにあたって多少のコード量は欲しかったので)

・ [mao-test-h/DOTS-Jungle](https://github.com/mao-test-h/DOTS-Jungle)

**環境/スペック**

- macOS Catalina 10.15.3
    - MacBook Pro (15-inch, 2018)
        - CPU: 2.2 GHz 6コア Intel Core i7
        - メモリ 16GB 2400 MHz DDR4
- Unity2019.3.11f1
- Xcode 11.4.1


### 検証内容

以下に検証項目と期待する動作を定義します。
こちらを`ccache -zC`でキャッシュ及び統計情報を消した状態で上から順にビルドしていき、ビルド時間やキャッシュヒット率などの変動を見ていきます。

- **①: Unityから出力された`xcodeproj`で「Build → Clean」を3回繰り返す**
    - → そもそも`ccache`は機能するのか？
- **②: ①から継続して「Clean → Archive」を行う**
    - → ①で得たキャッシュはアーカイブ時にもヒットするのか？
- **③: ①, ②でビルドした`xcodeproj`に対し、Unity側でAppendビルド**
    - → Unity側でAppendした際にキャッシュは維持されるのか？
- **④: ③の`xcodeproj`に対し、Unity側でReplaceビルド**
    - → `xcodeproj`を丸々置き換えた際にもキャッシュは維持されるのか？


ちなみに今回は「そもそもとして機能するのか？」に注目しているので、(検証としては微妙かもしれませんが...)ビルド毎にコード変更などは加えてません。
記載しているビルド時間はそれを踏まえた結果として見てください。


### ビルド設定

デフォルトから変更した項目のみ記載します。
各項目の設定についてはドキュメントをご覧ください。

```
compiler_check = content
sloppiness = pch_defines,time_macros,clang_index_store
```

### ①の結果

**2回目からキャッシュヒット率が上がっており、ビルド時間が大幅に短くなっていることが確認できました。**
この事から`ccache`自体は正しく機能しているみたいです。

|`ccache -s`|1回目|2回目|3回目|
|:--:|:--|:--|:--|
|**ビルド時間(秒)**|               **202.0sec**|    **25.4sec**|    **24.1sec**|
|**cache hit (direct)**|          0|           404|        808|
|**cache hit (preprocessed)**|    0|           5|          5|
|**cache miss**|                  409|         409|        414|
|**cache hit rate**|              **0.00 %**|      **50.00 %**|    **66.26 %**|
|**cleanups performed**|          0|           0|          0|
|**files in cache**|              1913|        1913|       1937|
|**cache size**|                  583.7 MB|    583.7 MB|   584.2 MB|
|**max cache size**|              5.0 GB|      5.0 GB|     5.0 GB|                   


### ②の結果

①の状態から継続してアーカイブを行ってますが、**1回目の結果を見る分にはキャッシュヒットが上昇していない上にキャッシュミスが目立っているので、恐らくは①のキャッシュが参照されていないように思われました。** (キャッシュミスの理由についてはまだ未調査...)

2回目からは新たに生成されたキャッシュを見に行っているためか、再びヒット率が上昇してビルド時間が短くなってます。

|`ccache -s`|1回目|2回目|3回目|
|:--:|:--|:--|:--|
|**ビルド時間(秒)**|               **308.7sec**|    **140.6sec**|    **132.5sec**|
|**cache hit (direct)**|          **808 (prev:808)**|         **1211**|        **1620**|
|**cache hit (preprocessed)**|    5|           5|           5|
|**cache miss**|                  **823 (prev:414)**|         **829**|         **829**|
|**cache hit rate**|              49.69 %|     59.46 %|     66.22 %|
|**cleanups performed**|          0|           0|           0|
|**files in cache**|              3849|        3877|        3877|
|**cache size**|                  1.8 GB|     1.8 GB|       1.8 GB|
|**max cache size**|              5.0 GB|     5.0 GB|       5.0 GB|
              


### ③の結果

Unity上からAppendして`xcodeproj`を更新した際の結果です。
ビルド時間やキャッシュヒット/ミスの数値を見た感じだと、**①と②で生成されたキャッシュがちゃんとヒットしているように思われます。**

|`ccache -s`|1回目(Clean → Build)|2回目(Clean → Archive)|
|:--:|:--|:--|
|**ビルド時間(秒)**|               **25.0sec**|    **142.1sec**|
|**cache hit (direct)**|          **2029 (prev:1620)**|       **2438**|
|**cache hit (preprocessed)**|    5|          5|
|**cache miss**|                  **829 (prev:829)**|        **829**|
|**cache hit rate**|              **71.04 %**|    **74.66 %**|
|**cleanups performed**|          0|          0|
|**files in cache**|              3877|       3877|
|**cache size**|                  1.8 GB|     1.8 GB|
|**max cache size**|              5.0 GB|     5.0 GB|
               

### ④の結果

最後にUnity上からReplaceして`xcodeproj`を置き換えた際の結果です。
(ちなみに「既存の`xcodeproj`を手動で削除 → ビルド」の手動Replaceも同様の結果です)

**こちらはBuild/Archive共に殆どキャッシュヒットしていない様に見受けられました。**

`ccache`の設定は以下の様になっており、更新チェックに関する条件自体は問題無さそうに思われますが...なぜ適用されていないのかまでは追いきれておらず...。

- `base_dir`は設定していないので、対象は絶対パスで制御される
    - → Unityから出力される`xcodeproj`は常に同名で出力しており、パス自体は変わっていないので適用されるはず...
- 更新ファイルのチェック方法は`content(ファイル内容を基にチェック)`で設定
    - **→ チェック方法をデフォルトの`mtime(mtimeとsizeのhash)`に戻しても変わらず...**
    - 出力されたIL2CPPのコードにも目立った差分は無い

|`ccache -s`|1回目(Clean → Build)|2回目(Clean → Archive)|
|:--:|:--|:--|
|**ビルド時間(秒)**|               **195.6sec**|    **301.8sec**|
|**cache hit (direct)**|          **2439 (prev:2438)**|        **2440**|
|**cache hit (preprocessed)**|    10|          10|
|**cache miss**|                  **1232 (prev:829)**|        **1640**|
|**cache hit rate**|              **66.53 %**|     **59.90 %**|
|**cleanups performed**|          0|           2|
|**files in cache**|              5350|        6576|
|**cache size**|                  2.4 GB|      3.4 GB|
|**max cache size**|              5.0 GB|      5.0 GB|


### 結果を踏まえて

個人的には「Replace相当のビルドを行った際にもキャッシュヒットしてくれると嬉しい[^3]」と言うモチベーションから調査を開始したこともあったので、今の時点での結果としては少し微妙だった...と言ったところです。

[^3]: 例えばビルドプロセスを自動化する際に、ビルド毎に影響が残らないように敢えてReplace相当でビルドすることがあるので、ここでキャッシュヒットして高速化されると嬉しかったりする。

原因については特定しきれていないので、気が向いたら調査を再開していきたい...。

#### 未調査項目

幾つか挙げます。

- Xcode側のビルド設定の詳細把握
- Xcode/ccache共に設定に抜けが無いか確認
- ドキュメントにある「Troubleshooting -> Performance」を読む分にはマクロ周りが影響を及ぼしている可能性も考えられる...?
    - 後は「揮発性の情報を含む自動生成コード」と言うのも引っ掛かってそうな感じもあったり... :thinking:
- 「Cache debugging」にあるデバッグモードを有効にすることで原因調査を進められそう？
    - まだ未検証...







# 補足: Androidは？

「内部的にNDK使ってそうだし、一部のビルドプロセスは高速化出来るかな？」と仮説立てて調べてみましたが...結論から言えばダメそう...?(調査不足の可能性あり)

環境変数の`NDK_CCACHE`などを設定した状態でAndroidビルドを回してみましたが、`ccache -s`で統計情報を見る分には何も動いていないように思われました。

後はこの状態で`[Export Project]`をON/OFFにしてビルドを行い、双方のビルドレポートを確認した感じだと、どちらも支配的なのは`IL2CPP`と言うプロセスでした。
こちらが文字通り「ILからCPPに変換するツール側のプロセス」でコンパイラが一体関与しない類のものであれば、そもそもとして適用自体でき無さそうとも考えられそうです。(IL2CPPをコードレベルで追ったわけではないので予想にはなりますが...)

![EXTuVImUYAA6TjB.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80207/786bdf17-a7ca-a1db-c695-623ed9edf8c0.png)










# 参考/関連サイト

- [ccache.dev(公式サイト)](https://ccache.dev/)
    - [ver3.7.9のドキュメント](https://ccache.dev/manual/3.7.9.html)
- [Ccache - ArchWiki](https://wiki.archlinux.jp/index.php/Ccache)
- [ccache でビルド高速化。と設定のポイント](https://qiita.com/naohikowatanabe/items/a6cb8745737481b103e3)
- [ccacheを導入してC++のコンパイルを最適化](https://qiita.com/tani-shi/items/e1493e63a02966ef1bac)
- [ccacheを使って共同作業のビルド時間を改善する](https://www.ibm.com/developerworks/jp/linux/library/l-ccache/index.html)
- [cocos2d-x(Android NDK)ビルドを高速化する方法とオススメ設定](http://matsukaz.hatenablog.com/entry/2015/02/04/094741)
- [C/C++のコンパイルを高速化する｜ccache](https://nagayasu-shinya.com/gmake-ccache/)