今是昨非

今是昨非

日出江花红胜火,春来江水绿如蓝

iOS コンパイル時間の最適化

背景#

現在、App プロジェクトはそれほど大きくありませんが、クリーン後に再コンパイルするのに 200 秒以上かかり、あまり合理的ではないと感じたため、調査を始めました。

通常のコンパイル時間の最適化は、3 つの部分に分かれています。

  • Xcode のコンパイル設定の最適化
  • コードまたは関数のコンパイル時間の最適化
  • サードパーティライブラリのコンパイル時間の最適化

ここでは、上記の 3 つの部分に基づいて一つずつ調査します。

実装#

Xcode のコンパイル設定の最適化#

使用しているのは Xcode 13.4 で、インターネットで見つけた Xcode の設定New Build SystemBuild SettingsでのDebug Information Formatの設定は、特に設定する必要はなく、デフォルトで合理的です。Optimization Levelの設定については、設定後にコンパイル速度が向上するものの、デバッグには不向きなので、ここでは設定しません。したがって、この最適化に関しては何も処理していません。

コードまたは関数のコンパイル時間の最適化#

この部分は主に Swift に関するもので、まずコンパイルにかかる時間が長いメソッドを表示させるために、Build SettingsOther Swift Flagsに以下の設定を追加します。これは、200ms を超える関数や 300ms を超える型チェックの関数に警告を表示するという意味です。ここでの 200ms は自分で設定したもので、プロジェクトの実際の状況に応じて設定できます:


-Xfrontend -warn-long-function-bodies=200
-Xfrontend -warn-long-expression-type-checking=200

筆者がここで修正した内容は以下の 4 つです:

最適化待ちのコード例 1:


let count:Int = Int((self?.listParamItem.pageSize ?? 0) * ((self?.needRefreshPageNum ?? 0) - 1))
let endIndex = count + Int(self?.listParamItem.pageSize ?? 0) - 1
if (self?.dataList.count ?? 0) > endIndex {

上記のコードでは、オプショナルと??を混用し、型変換を行ってから比較しています。コード自体は間違っていませんが、コンパイルに非常に時間がかかります。これらのメソッドは 500ms を超えています。これを以下のコードに修正したところ、コンパイル時間は 100ms 以内に短縮され、5 倍の削減が実現しました:


if let self = self {
  let count: Int = self.listParamItem.pageSize * (self.needRefreshPageNum - 1)
  let endIndex: Int = count + self.listParamItem.pageSize - 1
  if self.dataList.count > endIndex {
}

最適化待ちのコード例 2:


let dic = [
    "aaa": xxx ?? yyy, 
    "bbb": ["ccc": "xxx", "eee": 5],
    "ddd": 5
]

上記のコードは一見問題がないように見えますが、コンパイル時間が 200ms を超えています。これは型推論が原因かもしれません。以下のコードに修正したところ、コンパイル時間は 200ms を超えなくなりました:


var dic1: [String: Any] = [:]
dic1["ccc"] = "xxx"
dic1["eee"] = 5

var dic2: [String: Any] = [:]
dic2["aaa"] = xxx
dic2["bbb"] = dic1
dic2["ddd"] = 5

最適化待ちのコード例 3


if type == .aaa ||
type == .bbb ||
type == .ccc ||
type == .ddd ||
type == .eee ||
type == .xxx {
  doSomething()
} else {
    doAnotherThing()
}

上記のコードは優雅ではない上に、コンパイルにも時間がかかります。これをSwitch caseに修正すると以下のようになります:


switch type {
case .aaa,
.bbb,
.ccc,
.ddd,
.eee,
.xxx: 
  doSomething()
default:
    doAnotherThing()
}

最適化待ちのコード例 4


let fontAdd: CGFloat = 14.0

protocolBtn.snp.makeConstraints { make in
    make.left.equalTo(agreeLabel.snp.right).offset(1)
    make.centerY.equalTo(checkBtn.snp.centerY)
    make.width.equalTo(kTransitionW(150 + fontAdd * 10))
}

上記のコードでは、小数と整数の混合演算により、Swift が小数か整数かを推論する必要があります。以下のように修正したところ、コンパイル超過警告は消えました:


let fontAdd: CGFloat = 14.0
let width: CGFloat = 150.0 + fontAdd * 10.0

protocolBtn.snp.makeConstraints { make in
    make.left.equalTo(agreeLabel.snp.right).offset(1.0)
    make.centerY.equalTo(checkBtn.snp.centerY)
    make.width.equalTo(kTransitionW(width))
}

最後に、特に長いメソッドがある場合もコンパイル超過警告が発生することがあり、これを複数のサブメソッドに分割することで解決しました。

サードパーティライブラリの最適化#

プロジェクトに統合されているサードパーティライブラリはすべて CocoaPods を通じて統合されているため、クリーンビルド後は毎回サードパーティライブラリが再コンパイルされます。Xcode にコンパイル時間を表示させるために、ターミナルを開いて以下をコピーして実行し、Xcode を再起動します。


$ defaults write com.apple.dt.Xcode ShowBuildOperationDuration -bool YES

その後、クリーンして再ビルドすると、詳細なコンパイル時間が表示されます:

コンパイル時間

ここでのZLPhotoBrowserは Pod 方式でインストールされたサードパーティライブラリであり、このライブラリのソースファイルのコンパイル時間は 30 秒かかっています。。。このページでは各ライブラリのコンパイル時間とプロジェクトのコンパイル時間を詳細に確認でき、コンパイル時間が長いサードパーティライブラリについては、Carthage方式での導入を検討できます。Carthageは使用するサードパーティライブラリをダウンロードし、xcframeworkとしてコンパイルし、その生成されたxcframeworkをプロジェクトにインポートして使用するため、クリーンビルド後に再コンパイルされることはなく、コンパイル時間を節約できます。

まとめ#

コンパイル時間の最適化の 3 つの部分を以下にまとめます:

  • Xcode のコンパイル設定の最適化 —— 新しいバージョンの Xcode では設定不要
  • コードまたは関数のコンパイル時間の最適化 —— 型推論や複雑な計算の最適化、演算子の使用に注意が必要ですが、すべてのコードのコンパイル超過が最適化を必要とするわけではありません。コンパイルの最適化とコードの簡潔さ、優雅さ、Swift の特性との間で自分でバランスを選択する必要があります。
  • サードパーティライブラリのコンパイル時間の最適化 —— この最適化は推奨され、変更しないサードパーティライブラリについては、Carthage方式での導入に切り替えることで再コンパイル時間を減らすことができます。

皆さんは最適化の前に、まずプロジェクトのコンパイル時間がどこで遅れているのかを分析し、サードパーティライブラリのコンパイル時間が長すぎるのか、プロジェクトのソースファイルのコンパイル時間が長すぎるのかを分析し、それに応じてステップ 2 またはステップ 3 に重点を置くことを決定してください。

参考#

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。