Swiftの型チェックが遅くなる理由を探求してみたときの話

はじめまして!LINE Fukuokaでインターン生としてiOSエンジニアをしています、秋勇紀(freddi)と申します。

LINE Advent Calendar 2018の9日目の記事として、Swiftのコンパイルの時間削減にチャレンジした話をします。どうぞよろしくお願いします!

本記事では、どのようなコードでSwiftの型チェックが遅くなるのか、そしてなぜなのか、を考えた時の話を書いています!

検証準備

実際の修正の流れを見る前に、Xcodeでのビルド時に型チェックにかかりすぎている部分を可視化してみましょう。

コード中の型チェックにかかる時間で警告を表示する

関数ごとに型チェックがかかる時間を警告として出すのは、XcodeのプロジェクトからBuild Settings -> Other Swift Flagsで以下のFlagを追加すれば可能になります。

-Xfrontend -warn-long-function-bodies=1000

次のFlagを設定すると、コードごとの型チェックで警告を表示することができます。

-Xfrontend -warn-long-expression-type-checking=100

どちらのオプションも、型チェックが指定の値(単位はms)を超えると、警告が出るようになります。(ここでは関数だと1000ms、コードだと100msにしています)

プロジェクト全体でコンパイルにかかる時間を表示する

こちらはFlagによる設定ではなく、Shell上にて次のコマンドを入力した後、Xcodeを再起動すれば良いです。

defaults write com.apple.dt.Xcode ShowBuildOperationDuration YES

Xcodeの再起動後に再度ビルドをすれば、プロジェクトの全体のビルド時間がXcode上部に表示されるようになります。

実際の型チェックが遅いコードは?

では、型チェックが遅くなるコードを実際に見てみましょう!

普段よく書くようなコードが、実は型チェックを遅くする要因になるということがわかってきました。

※掲載しているコードは実際にプロダクトで修正したコードを改変したものです。

型チェックによる警告を覗いてみる

フルビルドを行うと、この関数がWarningとして引っかかりました。

private func _configureSomeLayout(_ collectionViewLayout: UICollectionViewFlowLayout) {
    collectionViewLayout.minimumLineSpacing = 16
    ...
    let width = (view.bounds.width - 10 * 2 - collectionViewLayout.minimumInteritemSpacing * 4) / 4
    ...
}

フルビルドかそうでないかで振れ幅がかなり大きいですが、警告を見ると型チェックに 約7~12秒 ほど時間がかかっている事がわかりました!

行ごとに見ていくと、主に

    let width = (view.bounds.width - 10 * 2 - collectionViewLayout.minimumInteritemSpacing * 4) / 4

に関数の警告とほぼ変わらない多くの時間を要していました。

さて、どのようにしてこの型チェックにかかる時間を減らすことができるのでしょうか?

型チェックの時間を減らすには?

CGFloatの型アノテーションを付ける

widthCGFloatの型アノテーションを付けてみました。これでどのくらい減るでしょうか?

private func _configureSomeLayout(_ collectionViewLayout: UICollectionViewFlowLayout) {
    collectionViewLayout.minimumLineSpacing = 16
    ...
    let width: CGFloat = (view.bounds.width - 10 * 2 - collectionViewLayout.minimumInteritemSpacing * 4) / 4
    ...
}

すると、型チェックの時間が2秒ほどにまで減りました!??

リテラルの型を明示してみる

width10 * 2の部分がCGFloatであることを明示してみましょう。

private func _configureSomeLayout(_ collectionViewLayout: UICollectionViewFlowLayout) {
    collectionViewLayout.minimumLineSpacing = 16
    ...
    let width: CGFloat = (view.bounds.width - CGFloat(10 * 2) - collectionViewLayout.minimumInteritemSpacing * 4) / 4
    ...
}

すると、警告がそもそも出なくなったので、型チェックの時間が1秒以下になりました!?????

なぜ型チェックに時間がかかるの?

さて、ここまでは修正した当時はほぼ憶測で修正したものでした。しかし、感覚としては CGFloatとリテラル の演算に違和感を持ったことが修正への大きなヒントとなりました。

view.bounds.widthCGFloatを返すものですが、その後に数値リテラルが続いていました。

... view.bounds.width - 10 * 2 ...

もしかしたら、10 * 2CGFloat型としてみなすまでに時間がかかるのかと思い、試しに... view.bounds.width - 10 * 2 * 5 ...とリテラルの計算を追加してみたところ、さらに 型チェックの時間が増えました

Instance method '_configure' took 24857ms to type-check ...

12秒 -> 24秒 どうやら、数値リテラルなどの型推論での走査に計算が増えてしまう ことが原因のようです。

ちなみに、リテラルの計算を追加したときに型アノテーションを外すとコンパイルエラーになりました。

The compiler is unable to type-check this expression in reasonable time; ...

どうやら、型アノテーションを外すと、型チェックの時間が爆発的に増える ようです。

修正したけど、そもそも全体のビルド時間は短縮されたの?

さて、一番気になるのが、全体のビルド時間は本当に短縮されたどうかです。
以下の表に、Clean->Bulidの計測時間をそれぞれ10回ずつ行った結果をまとめてみました。

今回の修正なしのビルド[秒] 修正ありのビルド[秒]
58.71 51.70
55.72 50.69
55.04 50.38
54.14 50.31
53.90 49.99
53.88 49.79
53.50 49.70
52.44 49.31
51.41 48.83
51.22 47.89

表では結果を降順にソートしています。

平均で、修正なしのビルドが 53.996秒 、修正ありのビルドが 49.859秒 でした。
それぞれの平均との差は-4秒程度でした。表で見ても、ちょっとだけ減った気がしますね。

ちょっとした修正で秒レベルの時間の削減ができました!少しだけ、これからのビルド時間の削減の道筋が見えるような結果になりました。

終わりに

今回の修正の結果では、あまり減った実感と量はそこまで多くないですが、これから先もっと大きくなるプロダクトなので、これからもどんどんビルド時間の削減に励みたいと思います!

みなさんが開発しているプロジェクトでもビルド時間の削減に励んでみて、削減したときのTipsをどんどん広めていただければ!と思います ???

さて、次の日の記事は、tom__boさんによる「MySQLのperformance-schema-instruments利用によるパフォーマンス影響を調べてみた」です。こちらもパフォーマンスの話なので、とても興味深いですね!みなさんもお楽しみに!


(2018年12月20日追記)
今回の記事の続編を、今年のSwift Advent Calendar 14日目の記事として書かせていただきました。

https://qiita.com/freddi_/items/044bdf6defbbe434a8e2

Swiftの数値リテラルの基本からコアな部分まで、更に今回の型チェックに時間がかかる理由にも関わることも解説していますので、ぜひご覧ください。

Related Post