WWDC Quick Look 💓 By SwiftGGTeam
Explore Swift performance

Explore Swift performance

元の動画を見る

ハイライト

Swift の性能直感は C ほど直接的ではない。C から機械語への変換はほぼ字義通り——ローカル変数はスタック上、ヒープ割り当ては明示的 malloc 呼び出し時のみ。Swift は安全性と大量の抽象ツール(クロージャ、ジェネリクス等)を導入し、その実装コストは malloc のように一目瞭然ではない。


主要内容

C を書くとき、性能直感は自然にある:ローカル変数はスタック、malloc が唯一のヒープ割り当て点、コンパイラの仕事はほぼ機械語と 1 対 1 対応。Swift はこの直感を破る——安全性要件が暗黙チェックを導入、ジェネリクス、クロージャ、プロトコル等の抽象ツールの実装コストがあちこちに散在し、malloc のように一目で見えない。

John McCall は低レベル性能問題を 4 次元に整理:関数呼び出しオーバーヘッド、データ表現効率、メモリ割り当てコスト、値のコピーと破棄コスト。各 Swift 機能が 1–2 次元に影響。良いニュース:Swift オプティマイザは強力で、見えない抽象オーバーヘッドの多くを排除。悪いニュース:オプティマイザに限界があり、コードの書き方が直接どれだけ最適化できるかを決める。

実用的助言:性能がプロジェクトで重要なら定期的に監視。トップダウン調査でホットスポット特定後、測定方法を見つけ、開発フローに自動化。オプティマイザ仮定を壊しても、二次アルゴリズムを偶発導入しても、自動回帰テストがタイムリーに捕捉。


詳細

関数呼び出しの 4 コスト

関数呼び出しには 4 オーバーヘッド:パラメータ設定、関数アドレス解決、ローカル状態空間割り当て、最適化の阻害。前 3 つは「何をしたか」、第 4 は「その結果何ができないか」(04:39)。

パラメータ渡しは 2 層:低レベルは呼び出し規約のレジスタ移動——現代プロセッサはほぼ隠蔽。高レベルは所有権規約が追加 retain/release を引き起こす可能性——プロファイルに頻出。

静的ディスパッチ vs 動的ディスパッチ が重要分岐。静的ディスパッチはコンパイラが関数定義を見てインラインとジェネリック特化可能。動的ディスパッチは多態をサポートするが最適化機会を犠牲。Swift では protocol requirement が動的、protocol extension method が静的(06:40):

protocol DataModel {
    func update(from source: DataSource)
}

extension DataModel {
    func update(from source: DataSource) {
        self.update(from: source, quickly: true)
    }
}
  • protocol DataModel 本体で宣言された update(from:) は requirement——呼び出しは動的ディスパッチ
  • extension DataModel で宣言された update(from:) は extension method——呼び出しは静的ディスパッチ
  • 意味が異なる:requirement は動的多態、extension method はコンパイル時既知型ベース
  • 性能クリティカルパスでは extension method 優先、ただし意味変化を理解

メモリ割り当て:グローバル、スタック、ヒープ 3 ソース

グローバルメモリはプログラムロード時に割り当て——ほぼ無料だが、プログラム全体を貫くライフサイクルの固定サイズデータのみ。スタック割り当てはスタックポインタ移動——同様に極速だが、現在関数スコープ内で明確なライフサイクル終点が必要。ヒープ割り当てが最も柔軟、割り当て/解放コストも最大(08:29)。

関数の CallFrame はスタック上に割り当て。コンパイラは戻りアドレス保存のためスタックポインタ減算を必ず 1 回実行——数バイト多減算しても追加時間なし。CallFrame 内ローカル変数はほぼ無料(07:18):

_$s4main9updateAll6models4fromySayAA9DataModel_pG_AA0F6SourceCtF:
    sub   sp, sp, #208
    stp   x29, x30, [sp, #192]

    ldp   x29, x30, [sp, 192]
    add   sp, sp, #208
    ret
  • sub sp, sp, #208:関数進入時、スタックポインタ 208 バイト減、CallFrame 割り当て
  • stp x29, x30, [sp, #192]:フレームポインタと戻りアドレス保存
  • add sp, sp, #208:関数退出時、スタックポインタ 208 バイト加算、CallFrame 解放
  • 208 バイトは modelssourcemodel、イテレータ等すべてのローカル状態をカバー

値のコピーと所有権

Swift 所有権システムはメモリ安全の核心。値の相互作用は 3 方式:consume(所有権移転)、mutate(一時所有取得後返却)、borrow(読み取り専用借用、他者の変更/破棄を阻止)(15:00)。

値コピーはインライン表現のコピー。struct コピーは全 stored property を再帰コピー、class コピーは参照 retain。大 struct コピーコストは 2 面:参照型 property は個別 retain、各コピー副本は独立ストレージ必要。consume 演算子で明示的コピーなし移転を要求(16:27):

func makeArray() {
    var array = [ 1.0, 2.0 ]
    var array2 = consume array
}
  • var array = [1.0, 2.0]:初期化。リテラルが独立値を生成、所有権直接移転、コピーなし
  • var array2 = consume arrayarray を明示 consume、array2 に所有権移転、ゼロコピー
  • consumearray アクセスはコンパイルエラー——値は移転済み
  • コンパイラは後続使用がないと証明できれば通常自動最適化。consume は手動制御手段

パラメータ渡し時、Swift は同時 mutate/consume がないことを証明して借用可能に。class property ストレージ位置が証明を困難にし、防御的コピーを引き起こす可能性(18:10):

func makeArray(object: MyClass) {
    object.array = [ 1.0, 2.0 ]
    print(object.array)
}
  • object.array ストレージは class インスタンス内。Swift は print 中に property が変更されないことを証明困難
  • コンパイラが防御的コピー(buffer retain)挿入の可能性——print が安定値を読む保証
  • Swift はオプティマイザ強化と明示 borrow 機能を含めこの領域を積極改善中

クロージャ:非エスケープ vs エスケープ

Swift の関数値は常にペア:関数ポインタ + コンテキストポインタ。非エスケープクロージャのコンテキストはスタック割り当て可能——呼び出し終了後クロージャは使用されない(28:47):

func sumTwice(f: () -> Int) -> Int {
  return f() + f()
}

func puzzle(n: Int) -> Int {
  return sumTwice { n + 1 }
}
  • sumTwice は非エスケープ関数 f を受け取る
  • クロージャ { n + 1 }n をキャプチャ。コンパイラが n を保持するスタックコンテキスト struct を生成
  • f() 呼び出し時、コンテキストポインタが暗黙パラメータとして渡される——ヒープ割り当て不要
  • nvar でクロージャが変更する場合、非エスケープクロージャは変数へのポインタのみキャプチャ——変数自体のヒープ化不要

エスケープクロージャは異なる:コンテキストはヒープ割り当て + 参照カウント管理——本質的に匿名 class インスタンス。エスケープクロージャがキャプチャする var もヒープ化——クロージャが変数ライフサイクルを延長する可能性(29:34)。

ジェネリクス vs プロトコル型(some vs any

ジェネリック関数とプロトコル型関数は似て見えるが、実行時特性は大きく異なる(31:50):

func updateAll<Model: DataModel>(models: [Model],
                            from source: DataSource) {
    for model in models {
        model.update(from: source)
    }
}

func updateAll(models: [any DataModel], from source: DataSource) {
    for model in models {
        model.update(from: source)
    }
}
  • 第 1(ジェネリック):配列要素同質、メモリコンパクト配置。型情報と witness table はトップレベルパラメータとして 1 回渡し。呼び出し側が具体型を知れば特化可能(32:57
  • 第 2(any):配列要素異質可能。各要素が独自型情報と witness table 持つ。インラインストレージは 3 ポインタサイズのみ、超過でヒープ割り当て。特化は極めて困難
  • any DataModel インライン表現:値ストレージ(3 ポインタ)、型メタデータポインタ、witness table ポインタ

オプティマイザはジェネリック関数を特化可能——呼び出し側が既知型 [MyDataModel] を渡せば、コンパイラがその型のみ処理する専用版を生成、すべての抽象オーバーヘッド排除。

async 関数のメモリモデル

async 関数のローカル状態は C スタック上に置けない——サスペンド中にスレッドが解放される可能性。Swift の方式は各 async task に独立 slab スタックを割り当て(25:02)。async 関数は複数 partial function に分割。各 partial function は C スタック上で次のサスペンドポイントまで実行し、次 partial function を tail-call。サスペンド時即座に return、スレッド即座に再利用。


重要ポイント

  • 何をするか:ジェネリック制約に any ではなく some を使用

    • 価値:some は具体型情報を保持しコンパイラが特化とインライン可能。any は existential container を導入、各要素が 3 ポインタ + メタデータ + witness table、3 ポインタ超過でヒープ割り当て
    • 始め方:関数パラメータの [any Protocol] を確認。要素が常に同質なら <T: Protocol> または [some Protocol] に変更
  • 何をするか:protocol requirement と extension method を区別

    • 価値:requirement は動的ディスパッチ——コンパイラはインライン不可。extension method は静的ディスパッチ——インラインと特化可能。性能クリティカルパスで差が顕著
    • 始め方:ホットパスのプロトコルメソッド呼び出しを精査。性能重要呼び出しを requirement から extension へ移動、またはプロトコルをジェネリック制約に変更
  • 何をするか:class property を直接パラメータ渡ししない、または borrow を明示标注

    • 価値:class property ストレージ位置がコンパイラの同時変更なし証明を困難にし、防御的コピー(追加 retain/release)挿入の可能性。Swift は改善中だが現時点で注意必要
    • 始め方:Instruments でホットパスの retain/release 数確認。異常に多いコピー発見時、ローカル変数経由または明示 borrow を検討
  • 何をするか:性能回帰テストを自動化

    • 価値:オプティマイザは強力だが万能ではない。コードスタイル変化が静かに最適化仮定を破壊。手動確認は信頼性低——自動化のみが各コミットで回帰捕捉
    • 始め方:Instruments でホットスポット特定後、それらを測定する XCTest 性能テスト作成、CI パイプラインに統合

関連セッション

コメント

GitHub Issues · utterances