LoginSignup
167
171

More than 5 years have passed since last update.

Swiftのfinal・private・Whole Module Optimizationを理解しDynamic Dispatchを減らして、パフォーマンスを向上する

Last updated at Posted at 2015-07-03

追記(2016/10/23)

SE-0117: Allow distinguishing between public access and public overridabilitypublicの意味が少し変わって、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の子クラスがpointvelocityupdatePoint()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 OptimizationYesにすると、モジュールは同時にコンパイルされ、コンパイラーは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倍 も速くなったようです。

参考資料

167
171
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
167
171