追記(2016/10/23)
SE-0117: Allow distinguishing between public access and public overridabilityでpublic
の意味が少し変わって、open
も登場した関係で、本記事中のコードはSwift 3では少し変更必要そうです。
-
final
がデフォルトになったので指定不要になった - オーバーライド可能にするには
open
を明示が必要になった(これまではデフォルトopen
状態だった)
デフォルトでDynamic Dispatchが発生しにくくなった感じです。
表面上の書き方は少し変わりましたが、本質は変わりません。
少し違う観点ですが、新しい公式記事もあがっています:
Swift.org - Whole-Module Optimization in Swift 3
Swiftパフォーマンス周りの話題だと、Swift Optimization Levelの違いでかなり差が出るのはわりと有名かと思います:
Apples to apples, Part II · Jesse Squires
一方、Dynamic Dispatch
について、少し前(2015/4/9)にSwift公式ブログに載っていましたが、あまり話題になってないように思います。
特に個人的にWhole Module Optimization
について理解したい事情もあり、読み解いてみました。
まず、親クラスのメソッドやプロパティのオーバーライドを実現するために、dynamic dispatch
の仕組みが必要とのことです。これが実行時のオーバーヘッドとなります。
対象コード:
class ParticleModel {
var point = ( 0.0, 0.0 ) // C
var velocity = 100.0 // D
func updatePoint(newPoint: (Double, Double), newVelocity: Double) { // B
point = newPoint
velocity = newVelocity
}
func update(newP: (Double, Double), newV: Double) { // A
updatePoint(newP, newVelocity: newV)
}
}
var p = ParticleModel()
for i in stride(from: 0.0, through: 360, by: 1.0) {
p.update((i * sin(i), i), newV:i*1000)
}
Dynamic Dispath発生箇所
- A: Call update on p.
- B: Call updatePoint on p.
- C: Get the property point tuple of p.
- D: Get the property velocity of p.
どうしてDynamic Dispatchが発生するか
ParticleModel
の子クラスがpoint
・velocity
・updatePoint()
・update()
をオーバーライドしうるため、動的にどのクラスのものを呼べば良いか判断する必要が出てきます。
Dynamic Dispatchの処理のされ方
- メソッドテーブルから検索
- 間接的に呼び出し
- 直接呼び出しより遅い
- このために恩恵を受けられないコンパイル最適化が存在
Dynamic Dispatchの減らし方
オーバーライドされる必要が無いときは、final
キーワードを使う
-
final
は、クラス・メソッド・プロパティに指定できる - クラスに対して指定した時は、継承されないことをコンパイラーに明示する
- メソッド・プロパティに対して指定した時は、オーバーライドされないことをコンパイラーに明示する
以下に変更すると、
- point・verlocityプロパティ、updatePointメソッド: 直接呼び出しになる
- update: 間接的な呼び出し(dynamic dispatch)のまま
class ParticleModel {
final var point = ( x: 0.0, y: 0.0 )
final var velocity = 100.0
final func updatePoint(newPoint: (Double, Double), newVelocity: Double) {
point = newPoint
velocity = newVelocity
}
func update(newP: (Double, Double), newV: Double) {
updatePoint(newP, newVelocity: newV)
}
}
クラスに指定すると、継承自体が不可となります。
この時点で、プロパティ・メソッドはオーバーライド出来ないので、それらにはfinal付ける必要も無く直接呼び出しになります。
final class ParticleModel {
var point = ( x: 0.0, y: 0.0 )
var velocity = 100.0
...
private
キーワードでfinal
による直接呼び出し化と同等のことも可能
Swiftのprivate
はファイルスコープなので、同一ファイルに定義すれば、こんなことが出来ちゃいます。
class Parent {
private var value: String! { return "parent" }
}
class Child: Parent {
private override var value: String! { return "child" }
}
このParent
クラスとChild
が別ファイルに定義されていたらコンパイルエラーになります。
つまり、下記のように定義して、ParticleModel
クラスを継承するクラスが同じファイルに無ければ、private
で定義したプロパティ・メソッドはfinal
でもあるとみなされます。
class ParticleModel {
private var point = ( x: 0.0, y: 0.0 )
private var velocity = 100.0
private func updatePoint(newPoint: (Double, Double), newVelocity: Double) {
point = newPoint
velocity = newVelocity
}
func update(newP: (Double, Double), newV: Double) {
updatePoint(newP, newVelocity: newV)
}
}
同様に、クラスをprivate
にすれば、ParticleModel
クラスを継承するクラスが同じファイルに無ければ、継承自体不可能になるため、プロパティ・メソッドはオーバーライド出来ず、それらにはfinal付ける必要も無く直接呼び出しになります。
private class ParticleModel {
var point = ( x: 0.0, y: 0.0 )
var velocity = 100.0
// ...
}
Whole Module Optimization
をYesにする
Swiftのデフォルトアクセスレベルであるinternal
は、モジュール内アクセスが可能です。
Swiftは通常モジュールを構成しているファイルを別々にコンパイルするので、internal
なものがオーバーライドされるかが分からずdynamic dispatchされる状態になってしまいます。
しかし、Whole Module Optimization
をYes
にすると、モジュールは同時にコンパイルされ、コンパイラーはinternal
なものがオーバーライドされるかどうか(モジュール内でオーバーライドされているかどうか)が分かるようになります。
例えば、下記のようにしたとしましょう。
public
指定のため、クラスはモジュール外アクセスが可能です。
このとき、point・velocity・updatePointがモジュール内でオーバーライドされていなければ、オーバーライドされる可能性は無くなり、final
とみなされ、直接呼び出しになります。
public class ParticleModel {
var point = ( x: 0.0, y: 0.0 )
var velocity = 100.0
func updatePoint(newPoint: (Double, Double), newVelocity: Double) {
point = newPoint
velocity = newVelocity
}
public func update(newP: (Double, Double), newV: Double) {
updatePoint(newP, newVelocity: newV)
}
}
var p = ParticleModel()
for i in stride(from: 0.0, through: times, by: 1.0) {
p.update((i * sin(i), i), newV:i*1000)
}
モジュールの話なので、Embedded Framework使った場合に限った話と思いきや、メインアプリもモジュールでもあるので、多分それについても同様かなと思っています。
Xcode 6.3・Swift 1.2以降のインクリメンタルビルドとの関係
Xcode 6.3・Swift 1.2でインクリメンタルビルドが導入されて、ソースを少し書き換えた後のビルド速度がかなり上がりました。
しかし、Whole Module Optimization
をYesにすると、インクリメンタルビルドが行われなくなりビルドが遅くなります。
Debugビルド: NO、Releaseビルド: YESなどが良いですかね?
Swift Optimization Levelの問題と同様、リリースビルドすると挙動がおかしくなったりしそうですが(´・ω・`)
ただ、僕はこれに関してはまだ実行時の挙動の差異を実感したこと無いです。
参考: Swift 1.2 Update (Xcode 6.3 beta 2) - Performance - Human Friendly
Xcode 6.3.1でのエンバグに注意
また、Whole Module Optimization
は、Xcode 6.3.1ではエンバグのため、Yes
にしか指定できませんので注意です。
参考: Swift - Xcode 6系のバージョン間の微妙な差異まとめ - Qiita
final
指定の本来の目的はオーバーライド不可を明示すること
final
指定することでコンパイルレベルでオーバーライド不可になりますし、設計意図も示せます。
これが本来の目的で、副次的にパフォーマンス向上にも繋がる、という程度に捉えた方が良いと思っています。
どのくらいパフォーマンスに差が出るのか
The Importance of Being final
- Human Friendlyの"How much difference does it make?"に記載がありました。
まず、Swift Optimization LevelがNone
だと差がほぼ出ないようです。まあインライン化とかしないですし納得です。
Swiftコード最適化をより効きやすくするもの、と捉えると良いですかね。
Fastest
では、記事に書いてある比較では 17.04倍 も速くなったようです。