Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

追記(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倍 も速くなったようです。

参考資料

mono0926
プロフィール: https://stackoverflow.com/story/mono0926 以前開発していたJOIN US: http://joinus30.com 2015年に開発しててベストアプリにも選ばれたPlayer!: http://www.playerapp.tokyo
https://medium.com/@mono0926
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした