2016/11/25

Xcode8でビルドが遅すぎるのを解消した件(Swift パフォーマンス改善)

Qiitaにも同様の投稿をしています。
http://qiita.com/you_matz/items/e95f30023eccc8d96357

2016年11月現在、最新(Xcode8.1環境下での)のコンパイル時間の計測方法が見当たらないので分析方法まで調査した。
2分程度かかっていたビルドが10秒ほどに短縮できました。
※ビルドするマシンのスペック、設定、ファイル数、コードの書き方にもよるので、
n%,n秒速くなったというのは相対的な値なので予めご了承ください。

はじめに

100クラス弱のswiftのプロジェクトで2分強ビルドに時間がかかっていたので、おかしいなと思い、おそらく静的にベタ書きした多次元配列に型情報を与えていないからだろうなと分かっていたが、いい機会なので原因を調査してみた。

プロジェクトのビルド時間の計測

こちらを参考に(Swiftのメソッド毎のコンパイル時間を計測してビルド時間を短縮する)

単にビルドが遅いと言われても、実質どのくらい時間がかかっているかわからないので計測
コンソールで以下実行
defaults write com.apple.dt.Xcode ShowBuildOperationDuration YES
その後、Xcode リスタート、クリーン、ビルドすると
以下のように時間が0.036sと表示される
これは18ファイルだけのBuildTimeAnalyzerのMacアプリなので速い
スクリーンショット 2016-11-19 午後5.37.15.png
これくらいビルド時間が速いと気にしなくていいですが、ビルド時間が遅い場合は以下で計測

ファイル毎のコンパイル時間の計測

まずxctoolで試す(※Xcode8でxctoolのbuildコマンドは使えなくなっている)

xctoolでファイル毎のコンパイル時間、メソッド毎のコンパイル時間を計測しようとしたが使えない、、、(xctoolでのbuildはXcode7だけ)
https://github.com/facebook/xctool#building-xcode-7-only

普通にxcodebuildで

プロジェクトルートにて以下コマンド
xcodebuild -workspace YOUR-APP.xcworkspace/ -scheme YOUR-APP clean build OTHER_SWIFT_FLAGS="-Xfrontend -debug-time-function-bodies" | grep .[0-9]ms | grep -v ^0.[0-9]ms | sort -nr > buildResult.txt
clean build、 OTHER_SWIFT_FLAGS に -Xfrontend -debug-time-function-bodies を追加することでメソッド、プロパティ毎に計測可能となる
YOUR-APPは適宜変更してください。cocoapods使用の場合は-workspace ですが、
していない場合-project YOUR−APP.xcodeproj/
必要な行だけ抽出して、コンパイルに時間を用するファイル、メソッド、プロパティ順でソート

結果その1(全ファイル)

7644.0ms    /Users/Hoge/develop/iOS/App/Pods/PagingMenuController/Pod/Classes/MenuItemView.swift:189:18 private func layoutMultiLineLabel()
7643.1ms    /Users/Hoge/develop/iOS/App/Pods/PagingMenuController/Pod/Classes/MenuItemView.swift:189:18 private func layoutMultiLineLabel()
1158.6ms    /Users/Hoge/develop/iOS/App/Pods/Siren/Siren/Siren.swift:590:10 final func setAlertType() -> SirenAlertType
1122.0ms    /Users/Hoge/develop/iOS/App/Pods/Siren/Siren/Siren.swift:590:10 final func setAlertType() -> SirenAlertType
677.9ms     /Users/Hoge/develop/iOS/App/Pods/ObjectMapper/ObjectMapper/Core/Operators.swift:485:13  public func <- :="" mappable="" ransform="" transform.object="" transformtype="" where="">(inout left: Dictionary?, right: (Map, Transform))
671.2ms     /Users/Hoge/develop/iOS/App/Pods/ObjectMapper/ObjectMapper/Core/Operators.swift:485:13  public func <- :="" mappable="" ransform="" transform.object="" transformtype="" where="">(inout left: Dictionary?, right: (Map, Transform))
608.4ms     /Users/Hoge/develop/iOS/App/Pods/ObjectMapper/ObjectMapper/Core/Operators.swift:502:13  public func <- :="" mappable="" ransform="" transform.object="" transformtype="" where="">(inout left: Dictionary!, right: (Map, Transform))

pods内コンパイル時間も入っているけど、1ファイルだけで7.6秒はやばい、、

Build Time Analyzer for Xcodeで計測(こちらがおすすめ)

Xcode8になってからAlcatraz(PackageManager)が使えないので、そのままソースからビルドしてMacアプリを起動
ソースはこちらから
https://github.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode/releases
アプリを起動すると以下のウィンドウが立ち上がる
スクリーンショット 2016-11-20 午後1.51.01-1.png
instructionsに沿って、
Build Settings > Swift Compiler - Custom Flags
  1. OTHER_SWIFT_FLAGS に -Xfrontend -debug-time-function-bodies を追加
  2. クリーン
  3. ビルド
  4. 以下のようなファイル、メソッド毎のコンパイル時間が表示された画面が起動

結果その2(GUI)

スクリーンショット 2016-11-20 午後2.31.13.png
サンプルプロジェクトでの結果なので3秒とすぐです。結果1とは違いPods内は含まれません。
行選択でファイルまで飛ぶことができ、
per file にチェックでファイルごとの時間もわかる
これで、どのファイルのどの箇所がコンパイルするのに時間がかかるのか予測がつきます。

カスタムフラグを追加

2016/12/11追記 swift3.0以降で有効。カスタムフラグを追加
Build Time Analyzerを起動する必要がないので現時点ではこちらの方が便利かもしれません。
Build Settings > Swift Compiler - Custom Flags
OTHER_SWIFT_FLAGS に -Xfrontend と -warn-long-function-bodies=100 を追加
これによってコンパイルに100ms以上かかっている箇所をwarningで出してくれます。
以下のような表示です。
スクリーンショット 2016-12-12 午前10.11.30.png
5249msタイプチェックにかかっているとのこと、クリックして修正箇所に飛べます。
-warn-long-function-bodies=100の100はワーニングを出すかの敷居値なので、
1秒であれば1000, 0.5秒であれば500にしてもいい。
これらのオプションは将来的には予告なしにサポートされなくなる可能性もあります。

改善策

簡単にできるものから
※マシンの性能上げろとか買い換えろとか単純な策は誰でも思いつくのでなしです

設定レベル-事前にできること

Optimization Levelを見直す

Build Settingの中にApple LLVM8.0 - Code GenerationとSwift Compiler があります。
Optimization LevelがデフォルトでDebug None[-Onone]となっていますが、
まれにFastに設定している場合があるのでこれをDebug時はNoneとすることで最適化が行われず、ビルド時間が短くなります。リリースビルド用ではfast,Fast, Whole Module Optimization指定して最適化が行われるようにするべきです。
スクリーンショット 2016-11-19 午後11.47.06.png

User-DefinedにSWIFT_WHOLE_MODULE_OPTIMIZATIONを追加

Build Setting > Add User-Defined Settingに以下を追加
SWIFT_WHOLE_MODULE_OPTIMIZATION = YES
最適化がかかる場合、ビルド時間が半分程に短縮できました。
リリース用ビルドでは最適化がかかるのでテストサーバとか配布用サーバなどでこの設定は有効です。

コンパイルの並列化

Macのコア数を確認
system_profiler SPHardwareDataType
スクリーンショット 2016-11-19 午後8.01.49.png
コア数に応じて、同時実行数を指定
defaults write com.apple.dt.Xcode IDEBuildOperationMaxNumberOfConcurrentCompileTasks 2
これでビルド時間が半分以下に縮む

Pods内コンパイルをスキップ

これはあまりおすすめできませんが紹介しておきます。
  • Product->Scheme->Edit Schemeでスキーマの編集画面へ
  • 左のリストからBuildを選択し、右画面のFind Implicit Dependenciesのチェックを外す
※Pods内を変更してもビルドが走らないので注意
※Find Implicit Dependanciesにチェックをつけてクリーン、ビルド

CocoaPodsをCarthageに切り替える

Carthage対応のライブラリはPodsでインストールしない方がよいです
  • 事前にフレームワークを作成できるのでコンパイルなしでその分コンパイル時間を短縮できる
  • つまりPodsだとクリーンインストール毎にビルドし直す必要があるが、Carthageはビルドし直す必要がない

コードレベル

型推論させない

明示的に型を付与させる
例えば以下のようなDictionaryを型情報なしで書くと型推論が働いて、コンパイルに時間がかかるので、
BadDictionary.swift
    static let hogeDict =
        [
            [
            "hoge1" : [
                "hoge1-1",
                "hoge1-2",
                "hoge1-3",
                "hoge1-4",
                "hoge1-5",
                "hoge1-6"
                ]
            ],
            [
            "hoge2" : [
                "hoge2-1",
                "hoge2-2",
                ]
            ]
        ]

型を付与
GoodDictionary.swift
    static let hogeDict: [Dictionary<String, Array<String>>] =
        [
            [
            "hoge1" : [
                "hoge1-1",
                "hoge1-2",
                "hoge1-3",
                "hoge1-4",
                "hoge1-5",
                "hoge1-6"
                ]
            ],
            [
            "hoge2" : [
                "hoge2-1",
                "hoge2-2",
                ]
            ]
        ]

配列の結合は以下のように

配列内容にもよりますが、コンパイルに時間がかかる可能性があります。
文字列連結で複雑な連結の仕方をしている場合も同様。
BadArrayConcat.swift
// Compiles multiple seconds
let slowArray = array1 + array2 + array3 + array4 + array5
// Compiles very fast
let array = Array([array1, array2, array3, array4, array5].flatten())

または+で連結するよりappendで追加
BetterArrayConcat.swift
return ArrayOfStuff + [Stuff]
// rather than
ArrayOfStuff.append(stuff)
return ArrayOfStuff

??演算子は使用しない

これはだめ
NilCoalescingOperator.swift
let name = "\(someString ?? "")"

はじめの複雑な連結はやめて、適切にアンラップ
メソッド内、ビルド時間を99.4%削減
VariableConcat.swift
// Build time: 5238.3ms
return CGSize(width: size.width + (rightView?.bounds.width ?? 0) + (leftView?.bounds.width ?? 0) + 22, height: bounds.height)

// Build time: 32.4ms
var padding: CGFloat = 22
if let rightView = rightView {
    padding += rightView.bounds.width
}

if let leftView = leftView {
    padding += leftView.bounds.width
}
return CGSizeMake(size.width + padding, bounds.height)

アンラップの箇所はguard letでもよい
コード的にも読みやすいというのはコンパイラも解釈しやすいということでしょうか

三項演算子もだめ

92.9%削減
TernaryOperator.swift
// Build time: 239.0ms
let labelNames = type == 0 ? (1...5).map{type0ToString($0)} : (0...2).map{type1ToString($0)}

// Build time: 16.9ms
var labelNames: [String]
if type == 0 {
    labelNames = (1...5).map{type0ToString($0)}
} else {
    labelNames = (0...2).map{type1ToString($0)}
}

Closureの引数の型を明示する

上記の例の用に$0でアクセスできますが、型推論が働いてしまうので明示的に型を付与
Closure.swift
 let cities = ["kyoto", "tokyo", "new york", "bogota", "mumbai"]
 let numbers = [1, 3, 6, 9]

 zip(cities, numbers).forEach {
     print($0.0, $0.1)
 }

 // rather than   
 zip(cities, numbers).forEach { (str: String, num: Int) in
     print(str, num)
 }

多重キャスト

普通はこんなことしないと思いますが、CGFloatCGFloatにキャストしたり、
驚くべきごとに、うっかりミスのこの箇所だけで3.4秒かかってしまっていた。
MultiCast.swift
// Build time: 3431.7ms
return CGFloat(M_PI) * (CGFloat((hour + hourDelta + CGFloat(minute + minuteDelta) / 60) * 5) - 15) * unit / 180

// Build time: 3.0ms
return CGFloat(M_PI) * ((hour + hourDelta + (minute + minuteDelta) / 60) * 5 - 15) * unit / 180

lazy properties

Swift2.2では遅延プロパティをタイプチェックし、
ターゲット内のすべての単一の.swiftファイルの遅延プロパティのコンパイル時間は累積されます。Swift 3.0では、この問題は引き続き起こりますが、ビルド時間はほぼ半減しています。
遅延プロパティを使用している場合は、注意を払うことをお勧めします。
BadLazyProperties.swift
private(set) lazy var chartViewColors: [UIColor] = [
    self.chartColor,
    UIColor(red: 86/255, green: 84/255, blue: 124/255, alpha: 1),
    UIColor(red: 80/255, green: 88/255, blue: 92/255, alpha: 1),
    UIColor(red: 126/255, green: 191/255, blue: 189/255, alpha: 1),
    UIColor(red: 161/255, green: 77/255, blue: 63/255, alpha: 1),
    UIColor(red: 235/255, green: 185/255, blue: 120/255, alpha: 1),
    UIColor(red: 100/255, green: 126/255, blue: 159/255, alpha: 1),
    UIColor(red: 160/255, green: 209/255, blue: 109/255, alpha: 1),
    self.backgroundGradientView.upperColor
]

ビルド時間を改善するには、できるだけプライベートメソッドにコードを移動
GoodLazyProperties.swift
// Cumulative build time: 56.3ms
private(set) lazy var chartViewColors: [UIColor] = self.createChartViewColors()

// Build time: 6.2ms
private func createChartViewColors() -> [UIColor] {
    return [
        chartColor,
        UIColor(red: 86/255, green: 84/255, blue: 124/255, alpha: 1),
        UIColor(red: 80/255, green: 88/255, blue: 92/255, alpha: 1),
        UIColor(red: 126/255, green: 191/255, blue: 189/255, alpha: 1),
        UIColor(red: 161/255, green: 77/255, blue: 63/255, alpha: 1),
        UIColor(red: 235/255, green: 185/255, blue: 120/255, alpha: 1),
        UIColor(red: 100/255, green: 126/255, blue: 159/255, alpha: 1),
        UIColor(red: 160/255, green: 209/255, blue: 109/255, alpha: 1),
        backgroundGradientView.upperColor
    ]
}

上記の遅延プロパティは繰り返しタイプチェックを受け取りますが、コードを移動するとビルド時間が96.7%短縮されます。
参考: Swift build time optimizations — Part 2
https://medium.com/swift-programming/swift-build-time-optimizations-part-2-37b0a7514cbe#.pshxh3hjo

finalprivate 修飾子を付与

動的ディスパッチを減らすため(実行時のオーバーヘッドをなくし、パフォーマンスをあげる)
継承しないクラス、メソッド、プロパティに関してはfinalprivate 修飾子を付与
間接実行ではなく直接実行するので速い
  • overrideされることがなければ、直接実行されるので高速化
  • 適切にpublic internal final privateを使い分ける

StructEnumを使用

Structは継承できず、直接実行なので速い
structenumを組み合わせることで継承クラスのような振る舞いは可能
classはヒープ領域に、structはスタック領域にメモリ確保されるので速い
スタックはポインタ加算でメモリ確保、減算でメモリ破棄、シンプル
ヒープは「空き」を探しメモリ確保する、その領域に再挿入することでメモリ破棄
Heapは様々なスレッドからアクセスされるので「保護」する必要がある
このコストは決して小さくはない

最後に

型推論をさせない点で言えば、静的な型定義は高速化のポイントになりますが、冗長になり可読性が落ちるということもありえますので、分析結果をみてパフォーマンスが悪い部分のみ、型宣言するのでもいいかもしれません。
ビルド時間が遅いのはいらいらしますし、何よりコンパイラを混乱させないように、解釈しやすいようにコードを書くことはビルド時間の短縮、可読性を上げることにも繋がるのでおすすめです。Swift3.0でパフォーマンスが上がったことは間違いないですが、コンパイラにとって何が解釈しやすいかを調べることはこれからも重要でしょう。

参考

Profiling your Swift compilation times
http://irace.me/swift-profiling
Swiftのメソッド毎のコンパイル時間を計測してビルド時間を短縮するhttp://qiita.com/rizumita/items/913b05d799b3712260f6
【Swift】 それ、enumとstructでやってみましょう!!
http://www.slideshare.net/uin010/swift-enum