ccache で Unity の生成する Xcode プロジェクトのビルドを高速化する
ccacheを導入してUnityのiOSビルドを高速化出来ないか検証してみた を参考にさらに調査を進めたところ、Unityプロジェクトを再生成した場合でもうまくキャッシュヒットさせることができたため知見を共有します。
TL;DR
- 🎊 ccache を利用することで Unity の生成する Xcode プロジェクトのビルドを大幅に高速化することができます。
- サンプルプロジェクトではビルド時間が 50% になりました。
- 🎊 Xcode プロジェクトを再生成してもキャッシュヒットするため、CI上でのビルドも高速化することができます。
- 🌀 いくつかの未検証項目があります。
- 特にUnity側のソースコードに変化を加えた場合のキャッシュヒット率については未調査です。
導入の手順
生成された Xcode プロジェクトの Build Settings の内容によって必要な手順が異なります。
具体的には UnityFramework
ターゲットの Build Setings の Enable Modules (C and Objective-C)
が Yes なのか No なのかで異なります。
Enable Modules
の設定が No
の場合 (通常はこちら)
① ccache をインストールする
homebrew から ccache をインストールします。(執筆時点では v4.2.1 が最新)
$ brew install ccache
...
$ ccache --version
ccache version 4.2.1
ccache_wrapper
を用意する
②
ccache_wrapper
の作成
Unity プロジェクト(ないしUnityプロジェクトを含むGitリポジトリ) に ccache の呼び出しをラッパーするシェルスクリプト ccache_wrapper
を追加します。
#!/bin/bash
if type "ccache" > /dev/null 2>&1; then
export CCACHE_SLOPPINESS=pch_defines,time_macros
exec `which ccache` $DT_TOOLCHAIN_DIR/usr/bin/clang -Xclang -fno-pch-timestamp "$@"
else
exec $DT_TOOLCHAIN_DIR/usr/bin/clang "$@"
fi
実行権限の付与
ccache_wrapper
に実行権限を付与します。
$ chmod +x ccache_wrapper
何故 ccache_wrapper を用意するのか
ccache_wrapper
には以下の二つの目的があります。
- ccache がインストールされていない環境では ccache を使わずに、通常通りコンパイルを行うにする。
-
cache_wrapper
内で定義された環境変数を経由して ccache の設定を行う。
ccache_wrapper
を利用する設定を行う
③ Xcode プロジェクトで Unity の iOS ビルドによって生成された Xcode プロジェクトに ccache_wrapper
を利用する設定を行います。
ここではUnity エディタ拡張の OnPostprocessBuild
フックで Xcode プロジェクトを編集する方法で説明します。
エディタ拡張を追加する
using System.IO;
using UnityEditor;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;
using UnityEditor.iOS.Xcode;
using UnityEngine;
public class EnableCcache : IPostprocessBuildWithReport
{
// Assets ディレクトリから ccache_wrapper への相対パス (適宜書き換えてください)
private const string PathToCcacheWrapper = "../iOS/ccache_wrapper";
// OnPostprocessBuild の呼び出し順 (適宜書き換えてください)
public int callbackOrder { get; } = 0;
public void OnPostprocessBuild(BuildReport report)
{
if (report.summary.platformGroup != BuildTargetGroup.iOS) return;
// ccache_wrapper を Xcode プロジェクトと同じディレクトリにコピー
var srcPath = Path.Combine(Application.dataPath, PathToCcacheWrapper);
var dstPath = Path.Combine(report.summary.outputPath, "ccache_wrapper");
FileUtil.CopyFileOrDirectory(srcPath, dstPath);
// ccache_wrapper を利用してコンパルを行う設定を追加
var pbxProjectPath = PBXProject.GetPBXProjectPath(report.summary.outputPath);
var pbxProject = new PBXProject();
pbxProject.ReadFromFile(pbxProjectPath);
var projectGuild = pbxProject.ProjectGuid();
pbxProject.AddBuildProperty(projectGuild, "CC", "$(PROJECT_DIR)/ccache_wrapper");
pbxProject.WriteToFile(pbxProjectPath);
}
}
Cocoapods を利用している場合
External Dependency Manager を利用している場合など、 Cocoapods によって依存ライブラリをインストール&ビルドしているケースでは Pods.xcodeproj
にも ccache の設定が必要です。
(未確認ですが Pods.xcodeproj
に ccache の設定を行わなくても、Unity-iPhone.xcodeproj
側のビルドは ccache が有効になるはずです。)
Podfile への設定の追記
以下の設定を Podfile
に追記します。
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['CC'] = '$(PROJECT_DIR)/../ccache_wrapper'
end
end
end
Unity エディタ拡張から自動で追記する場合は、以下のようなC#スクリプトを追加します。
using System.IO;
using UnityEditor;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;
public class IOSBuildPostprocess : IPostprocessBuildWithReport
{
// OnPostprocessBuild の呼び出し順 (適宜書き換えてください)
// External Dependency Manager (EDM)を利用している場合は、EDM によるPodfile生成が 40 で pod install が 50 なのでその間の値にする必要があります。
public int callbackOrder { get; } = 45;
public void OnPostprocessBuild(BuildReport report)
{
if (report.summary.platformGroup != BuildTargetGroup.iOS) return;
var podfilePath = Path.Combine(outputRootPath, "Podfile");
var podInstallHook = @"
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['CC'] = '$(PROJECT_DIR)/../ccache_wrapper'
end
end
end
";
File.AppendAllText(podfilePath, podInstallHook);
}
}
Enable Modules
の設定が Yes
の場合
① パッチ版の ccache をインストールする
Enable Modules
の設定が Yes
の場合、コンパイル時の clang への引数に -ivfsoverlay
が追加されます。 通常の ccache は現在この引数に対応しておらずキャッシュが行われません。(ccache には multiple source files
と認識される。この場合はコンパイル結果がキャッシュ対象にならない。)
この問題を解決したパッチバージョンが Homebrew tap に存在する(PSPDFKit-labs/homebrew-tap)のでそちらを使う必要があります。
Homebrew tap でパッチバージョンをインストールする
$ brew tap pspdfkit-labs/tap
...
$ brew install ccache
...
$ ccache --version
ccache version v4.1.pspdfkit
ccache_wrapper
を用意する
② Enable Modules
の設定が Yes
の場合はいくつかの ccache 設定を追加する必要があるため、ccache_wrapper
の内容が異なります。
ccache_wrapper
の作成
Unity プロジェクト(ないしUnityプロジェクトを含むGitリポジトリ) に ccache の呼び出しをラッパーするシェルスクリプト ccache_wrapper
を追加します。
#!/bin/bash
if type "ccache" > /dev/null 2>&1; then
export CCACHE_SLOPPINESS=pch_defines,time_macros,modules,ivfsoverlay
export CCACHE_DEPEND=1
exec `which ccache` $DT_TOOLCHAIN_DIR/usr/bin/clang -Xclang -fno-pch-timestamp "$@"
else
exec $DT_TOOLCHAIN_DIR/usr/bin/clang "$@"
fi
実行権限の付与
ccache_wrapper
に実行権限を付与します。
$ chmod +x ccache_wrapper
ccache_wrapper
を利用する設定を行う
③ Xcode プロジェクトで 「Enable Modules
の設定が No
の場合」と同様です。そちらを参照してください。
サンプルプロジェクトでの検証結果
ccacheを導入してUnityのiOSビルドを高速化出来ないか検証してみた でも利用されている mao-test-h/DOTS-Jungle をお借りしました 🙏
検証手順
- Unity で iOS ビルドを行う
- 生成された Xcode プロジェクトを以下のコマンドでビルドして、ビルド時間を計測。
time xcodebuild -derivedDataPath DerivedData -project Unity-iPhone.xcodeproj -scheme Unity-iPhone
- Xcode プロジェクトを消してもう一度 Unity のiOS からやりなおす。
- この時 Xcode プロジェクトの生成先は同じパスにする。
検証結果
- ビルド時間
- 1回目
231.25 secs
→ 2回目121.27 secs
- 1回目
- キャッシュヒット率
399/400 = 99.75 %
1回目と2回目で、ビルド時間が 50% に短縮されました 🎊
短縮されていない時間はコンパイル以外の時間だと思われます。キャッシュヒット率が非常に高いので、規模が大きいプロジェクトほど効果が高くなるはずです。
検証環境
- Software version
- macOS Catalina 10.15.7
- Xcode 12.4
- Unity 2019.4.15f1
- Machine spec
- 2.4 GHz 8-Core Intel Core i9
- 32 GB 2667 MHz DDR4
ccache_wrapper の設定項目の解説
ccache のオプション
CCACHE_SLOPPINESS
pch_defines
time_macros
- この二つのオプションはプリコンパイル済みヘッダを利用している場合に必要なオプションです。
- Xcode では
Precompile Prefix Header
の設定がYes
になっている場合に必要です。
- Xcode では
- Unity が生成する Xcode プロジェクトではデフォルトでプリコンパイル済みヘッダを利用する設定になっているため基本的に必須のオプションです。
- ccache のマニュアルの Precomipled Headers に詳しく書いてあります。
modules
- C,C++ or Objective-C で Module機能(
@import
ディレクティブ)を利用している場合に必要なオプションです。 - Unity が生成する Xcode プロジェクトではデフォルトでModule機能は無効です。プロジェクト固有で有効にしている場合に設定する必要があります。
- ccache のマニュアルの C++ modules に詳しく書いてあります。
CCACHE_DEPEND
-
modules
の sloppiness を有効にするには、このオプションも有効にしておく必要があります。 - このオプションを有効にすると ccache が利用するキャッシュキーの生成ロジックが
Depend mode
に変更されます。- ccache のデフォルトでは
Preprocessor mode
というロジックを利用しています。これは#include
等のプリプロセッサを解釈したあとのソースファイルの内容を用いてハッシュを生成するロジックです。 -
Depend mode
ではコンパイル対象のソースコードが include しているファイルを抽出し、「include されているファイルのキャッシュキー」を元にキャッシュキーを生成します。
- ccache のデフォルトでは
- ccache のマニュアルの The depend mode に詳しく書いてあります。
CCCAHE_BASEDIR
- このオプションは ccache が認識するファイルパスを相対パスに書き換えるためのオプションです。
- ccache のデフォルトではビルド時のファイルパスを絶対パスで認識するため、 同一の ソースファイルを異なるディレクトリでビルドするとキャッシュヒットしない 状態になりますが、このオプションを指定して相対パスを一致させるとキャッシュヒットします。
- ただし、以下の理由で Unityの 生成した Xcode プロジェクトのビルドでは
CCACHE_BASE_DIR
の設定にほとんど意味がないため、この記事で紹介しているccache_wrapper
では設定していません。-
ccache
は、あるソースコードのコンパイル結果がキャッシュヒットするかどうかの判定の一項目に「インクルードしているpchファイルの内容に変化がないか」を利用している。 - clang のデフォルトの仕様として pch ファイルに「絶対パス でインクルードしているソースコードのパス」が含まれているため、CCCAHE_BASEDIR を設定したとしても pch のコンパイル結果が変わってしまいキャッシュミスしてしまう。
-
- ccache のマニュアルの base_dir に詳しく書いてあります。
clang のコンパイルオプション
ccache_wrapper
では -Xclang -fno-pch-timestamp
のコンパイルオプションを追加しています。このオプションをつけると「pchファイルにincludeしたファイルのmtimeが含まれない」ようになります。
このオプションは以下の理由で必要です。
-
ccache
は、あるソースコードのコンパイル結果がキャッシュヒットするかどうかの判定の一項目に「インクルードしているpchファイルの内容に変化がないか」を利用している。 - しかし、 clang のデフォルトの仕様として pch ファイルには「pchがinclude しているヘッダファイルの mtime」が書き込まれるため、mtime が変わると pch ファイルの内容が変化してしまう。
- Unity が生成する Xcode プロジェクトは pch が include する ヘッダファイルの mtime が毎回変わってしまう仕様。
-
Classes/Preprocessor.h
の mtime が毎回変わる。こいつは Unity 側のビルド設定によって内容が変わるので、毎回テンプレートから生成しているためと思われる。
-
- よって、 Unity で Xcode プロジェクトを生成し直してビルドするとほとんどキャッシュヒットしない という結果になる。
ccacheを導入してUnityのiOSビルドを高速化出来ないか検証してみた の記事で Xcodeプロジェクトを再生成するとキャッシュヒットしなくなっていたのは、このclang/ccacheの仕様が原因だったと思われます。
未検証の項目
- Unity 側のソースコード(C#スクリプト) に変更があった場合のキャッシュヒット率への影響 は未検証です。
-
Module Enable = YES
場合のデバッグシンボルへの影響。キャッシュヒットした場合 dSYM の生成時になんか warning がたくさん出ている。 -
元記事の方ではLDPLUSPLUS
の User-Defined Setting を Xcode プロジェクトに追加しているが、なぜ追加しているのかわかっていない🤔- コメント で mao さん (元記事の執筆者) から「cocos2d-xの設定を参考にしていた名残り」であり「Unity向けには不要」とのコメントをいただきました。ありがとうございます🙏
GitHub Actions でビルドを行う際のTips
キャッシュのサイズ上限を調整する & 圧縮設定を変更する
actions/cache@v2 はリポジトリあたり 5GB までしかキャッシュを持つことができないため、jobs.<job_id>.env
に以下の環境変数の設定を行うとよいです。
-
CCACHE_MAX_SIZE
を設定する。- ccache の利用するディスクサイズの最大値を設定することができます。
-
CCACHE_COMPRESSLEVEL
を設定する。- zstandard の圧縮レベルを設定することができます。デフォルトが
1
ですが最大20
まであげることができ、値が大きくなるほど圧縮率が高くなります (その分圧縮にかかる時間も増える)
- zstandard の圧縮レベルを設定することができます。デフォルトが
ccache のバイナリをリポジトリに含める
GitHub ホストランナーを利用している場合、毎回 homebrew から ccache をダウンロード&ビルドするのも時間がかかります。actions/cache@v2 でインストールしたバイナリをキャッシュしてもよいですが、ccache のバイナリは大したサイズではないのでリポジトリに含めてしまうのも手です。
その場合は install_name_tool
で ccache
が依存している動的リンクライブラリの参照パスを書き換えた上で、依存している動的リンクライブラリも一緒にリポジトリに含める必要があります。
$ brew isntall ccache
...
$ cp /usr/local/bin/ccache .
...
$ otool -L ccache
/usr/local/bin/ccache:
/usr/local/opt/zstd/lib/libzstd.1.dylib (compatibility version 1.0.0, current version 1.4.9)
/usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 902.1.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1281.100.1)
$ ls -al /usr/local/opt/zstd/lib/libzstd.1.dylib
lrwxr-xr-x 1 kohei.noda admin 19 Mar 3 07:20 /usr/local/opt/zstd/lib/libzstd.1.dylib@ -> libzstd.1.4.9.dylib
$ cp /usr/local/opt/zstd/lib/libzstd.1.4.9.dylib .
$ install_name_tool -change /usr/local/opt/zstd/lib/libzstd.1.dylib @loader_path/libzstd.1.4.9.dylib ccache
Discussion
元となる記事を書いたmaoと申します!
本記事の方、すごく参考になりました!
ありがとうございます!🙏 🙇 💦
以下、補足です。
こちらは自分が以下の記事を参考に導入し始めたときの名残です。。
その上で参考にした記事の方はcocos2d-x向けとなるために、Unity側には不要な設定でした。。
元記事の方はこの件を踏まえて修正しておきます!🙇
おお、読んでいただきありがとうございます! Unity向けに不要な設定である旨承知しました。ではこちらの記事でも修正しておきます。
ご連絡ありがとうございます 🙌