読者です 読者をやめる 読者になる 読者になる

cockscomblog?

cockscomb on hatena blog

Swift の Nil Coalescing Operator でコンパイルは遅くなるか

Swift Development

趣味のウェブブラウジングをしていると、Swift の ?? (nil coalescing operator) がコンパイルを遅くするのではないか、といった話題*1を見かけました。この演算子は、左辺の Optional<Wrapped> 型の値が Optional.none である場合に右辺の値を返すというもので、直感的にはこれがコンパイル時間を悪化させるとは思えません。経験から言えば、このようなケースでは大抵やや複雑な型推論が発生しており、それがコンパイル時間に支配的な影響を与えています。そうであるなら、人間が少し工夫して型のコンテキストを与えてやることで、計算機はずっとよいパフォーマンスを発揮できるはずです。

ごく簡単な例で実験してみましょう。以下のコードは、let view: UIView? があるとき、座標系における view のX座標を得ようとするものです。ただし viewnil を取る場合は 0 であることにします(nil coalescing operator を用います)。このとき得られる値 x の型は CGFloat になります。

let x = view?.frame.origin.x ?? 0

このように書いたとき、view?.frame.origin.xOptional<CGFloat> 型であるから、0 という数値リテラルCGFloat 型の値であり、結果として xCGFloat 型である、というような順で型推論は解決されるでしょう。これは実際に少し複雑なので、コンテキストを与えて緩和することができます。

let x: CGFloat = view?.frame.origin.x ?? 0

一例としてはこうです。これは先ほどの例に対して xCGFloat 型であることを明示しただけです。

さて、これが本当にコンパイル速度を改善するのか試験してみましょう。Swift コンパイラに少しオプション(-Xfrontend -debug-time-function-bodies)を渡すことで、関数毎にコンパイル時間を出力することができます。元の話題の記事にあるように、Xcode プラグインの形で公開されているツールを利用することもできます。

筆者がざっくりと試験した結果では、元のような例(testA())では実際にコンパイルに時間を要しました。その他の CGFloat 型であるというコンテキストを与えた場合 if let 文などを利用した場合はほとんど変わらず、その差はありませんでした。この傾向はキャッシュをクリアした上で数度同じ試験を繰り返しても変わりませんでした。(きちんと統計的に検定すれば有意差が得られるはずですが、横着して計算していません。)

また元の話題で遅いとされていたケースも、少し書き換えることで十分早くすることができました。

// 3580.1ms
return CGSize(width: size.width + (rightView?.bounds.width ?? 0) + (leftView?.bounds.width ?? 0) + 22, height: bounds.height)

// 5.5ms
return CGSize(width: size.width + (rightView?.bounds.width ?? 0) as CGFloat + (leftView?.bounds.width ?? 0) as CGFloat + 22, height: bounds.height)

現在のバージョンの Swift コンパイラでは、型推論がやや複雑になった場合に、明確なコンパイル速度の低下が見られ、その場合でもコンテキストを与えることにより型推論の範囲を限定して速度を改善できる、ということが言えるはずです。また nil coalescing operator がそのような状況を生み出しやすい可能性があります。

私見では、コンパイル時間に配慮して nil coalescing operator を使わないという判断をする必要はなく、必要に応じて型のコンテキストを与える程度で十分だと思います。それよりはコードが持つ本来の意図がより明確になるように記述するのがよいでしょう。

こちらからは以上になります。