WWDC25:Optimize CPU performance with Instruments

Instruments を用いたCPUレベルでのパフォーマンス最適化のテクニック。バイナリサーチアルゴリズムの実装例をもとに、CPU内部の動作をマインドセットとして3ステップに分割し詳解。図解もあって直感的で、非常に見応えあったものの、後半にいくにつれ難解で理解を放棄してしまった。


0:00 – Introduction & Agenda

  • パフォーマンス予測の困難さ
    • Swift ソースコードから実際の実行環境の間に抽象化レイヤーがある
      • 機械語化されCPU実行
      • ランタイム、サポートコード、フレームワーク、カーネルも含まれ抽象化レイヤーは把握しづらい
    • CPU の並列実行、順不同(out-of-order)で実行、メモリキャッシュによる複雑性

2:28 – Performance mindset

  • オープンマインド:先入観を持たず、予想外の原因を想定、データ収集による仮説検証
  • 他の原因の考慮
    • スレッドのブロッキング(ファイルや共有状態の待機)
    • API の誤用(QoS クラス、過剰なスレッド作成)
    • 効率性の改題に対しては、アルゴリズムやデータ構造の変更を検討
    • ツール活用しボトルネックを特定(Xcode Gauges / System Trace / Hangs)
  • マイクロ最適化の注意点
    • コードの複雑化による拡張や推論の阻害や、脆弱な compiler 最適化への依存の可能性あり
  • 最適化の優先順位
    • マイクロ最適化を避けられないか検討
      • 代替手法:コード削除、遅延実行、事前計算、キャッシュ
      • これらを採用できない場合、CPUでの実行速度の向上が必要 ← 今回の主題
  • ユーザー体験に最大の影響を与えるクリティカルパスに焦点をあてて最適化すべき
    • ユーザー体験だけでなく、長時間の実行による電力消費の可能性
  • 定量化の難しい逐次的な最適化の継続により、小さな改善が大きな改善につながる
    • 継続改善のためのデータ収集用コード:
      • search クロージャを一定時間継続するまで再帰呼び出し
      • OSSignposter: 最適化対象をツールで絞り込めるようにするため、クロージャ呼び出しに使用(category: .pointsOfInterest
      • ContinuousClock: タイミング計測、Date と異なりオーバーヘッド小

8:50 – Profilers

  • Time Profiler vs CPU Profiler
    • Time Profiler:タイマーに基づきの定期的サンプリング、エイリアシング問題あり
      • エイリアシング: 定期処理がサンプリングタイマーと同じ頻度で発生、Instruments 上で結果に偏り
    • CPU Profiler:CPU クロック周波数ベース、より正確で公平な重み付けが可能
    • CPU 最適化において、CPU サイクルの最大消費箇所を特定するは CPU Profiler を利用
  • CPU Profiler の使用
    • deferred mode での低オーバーヘッド記録
    • Points of Interest での範囲設定
    • 例:Instrtuments上でコールツリーから protocol witness、allocation のオーバーヘッドを特定
      • Array の代替に Span を検討

13:20 – Span

  • Span の利点
    • Collection の代替として連続メモリ配置の要素に最適化
    • base address と count のみのシンプルな構造
    • エスケープやリークが防止され安全
  • 変更と結果:該当型を Span に変更するだけで4倍の高速化を実現
  • Span の境界チェックによるオーバーヘッドへの影響を調べる必要あり → Processor Trace

14:05 – Processor Trace

  • 革新的な機能
    • ユーザー空間で実行される全ての命令を完全にトレース(sampling bias なし)
    • 1% のみのパフォーマンス影響で無視可能
    • M4 Mac/iPad Pro、A18 iPhone での対応
  • 設定:Privacy & Security > Developer Tools での有効化
  • 使用方法
    • 数秒間の短時間トレースを推奨:記録データは数秒数GBに及ぶ可能性
    • 単一インスタンスの最適化でも十分
  • flame graph の詳細
    • 実際の実行順序通りの表示
    • 色分け
      • 茶:システム
      • マゼンダ:Swift ランタイム・標準フレームワーク
      • 青:アプリバイナリかカスタムフレームワーク
  • 発見
    • bounds check ではなく protocol metadata overhead が問題
    • 汎用的な Comparable が使用型に特化されていなかった
    • @inlinable アノテーションの追加またはInt型への手動特化の必要性
  • 結果:手動特化でコードの汎用性は失うが 1.7倍の高速化

19:51 – Bottleneck analysis

  • CPU の動作に関するメンタルモデル:2つのフェーズ
    • 命令送信(Instruction Delivery)
      • 命令がフェッチされ、マイクロ操作にデコードしCPUが実行しやすくする
    • 命令処理(Instruction Processing)
      • Map and Schedule ユニットへ送信、ルーティングとディスパッチ
      • 実行ユニット or メモリアクセス必要なら Load-Store ユニットに割り当て
    • 上記を逐次実行はフェッチ再開まで時間がかかるため、パイプライン化し並列実行
      • GCDなど複数CPUの異なるOSスレッドアクセスと異なり、1つのCPUが時間的に有利を得る
    • ユニット間でのやりとりにより、並列処理制限、パイプライン操作が停止される可能性:ボトルネック
  • ボトルネックの特定
    • CPU Counters のプリセットモード
    • 4つのカテゴリーによる CPU パフォーマンスの分析
  • 段階的な分析
    • CPU Bottlenecks レーン:Discarded bottleneck の高い割合を発見
    • Discarded Sampling セル:branch prediction miss の特定
  • CPU の命令実行の順不同性
    • 命令完了後に並べ替えるので、順番に実行したように見える(CPU による分岐予測機能)
    • 以前の実行に一貫したパターンがないと、誤った経路を辿る可能性
    • 今回はランダム性のある値比較による分岐で、予測に問題が発生した可能性
  • 変更
    • 条件付きの移動命令(conditional move instruction)を使用し、別の命令分岐を回避
    • 早期 return の除去
    • 未チェックの算術演算(unchecked arithmetic)の採用(&+)
  • 結果:2倍の高速化
  • memory hierarchy の課題
    • 予測可能なアクセスパターンでメモリアクセスすることによる高速化
    • L1、L2 キャッシュ とメインメモリへのアクセス速度差
      • L2 キャッシュは CPU 外にありヘッドルームが大幅増大
      • メインメモリは L1 キャッシュと比較し50倍低速
    • キャッシュはメモリをキャッシュラインにグループ化(64-128 bytes)
      • 4バイト要求の命令でもより多くのデータを引っ張ってくる
    • 例のバイナリサーチにおける「キャッシュミス」問題
      • 要素を並び替えてキャッシュしやすくし、検索ポイントを同じキャッシュラインに配置:エイツィンガー・レイアウト(Eytzinger layout)
  • Eytzinger layout
    • キャッシュフレンドリーな要素配置
    • breadth-first traversal によるツリー構造
    • search 速度向上と in-order traversal 速度低下のトレードオフ

31:33 – Recap

  • 全体的な成果:25倍の高速化を実現
  • 段階的なアプローチ
    1. CPU Profiler:Collection から Span への変更
    2. Processor Trace:unspecialized generics の overhead 発見
    3. Bottleneck Analysis:micro-optimization による大幅な性能向上
  • 重要な順序:software overhead の解決 → CPU bottleneck の最適化

32:13 – Next steps

  • 実践的なアプローチ
    • データ収集とパフォーマンスマインドセット
    • 繰り返し測定可能なパフォーマンステストの作成
    • Instruments の継続的な使用

WWDC25:Optimize SwiftUI performance with Instruments

Instruments を用いた SwiftUI のパフォーマンス改善。スクロール時などに発生するラグ(hangs and hitches)について、SwiftUI の描画更新ロジックを図示しながら、ケースとその因果関係を説明してくれていて、解決方法も含め非常に参考になった。シンプルな作りのアプリでも、調べてみると実は似たような状況が眠っているのではないかと思った。


0:00 – Introduction & Agenda

  • パフォーマンス問題の症状(Hitch or Hang):レスポンシブ性の低下、アニメーションの停止・ジャンプ、スクロール遅延
  • Instruments での分析:SwiftUI コードがボトルネックとなっているケースに焦点

2:19 – Discover the SwiftUI instrument

  • SwiftUI テンプレートの構成
    • SwiftUI Instrument:SwiftUI 固有のパフォーマンス問題を特定
    • Time Profiler:CPU での作業をサンプリング
    • Hangs and Hitches instruments:アプリの応答性を追跡
  • SwiftUI Instrument トラックの構造
    • 調査時はまずここのトップレベルの内容を確認する
    • Update Groups:SwiftUI が作業している時間を表示
      • ここが空いていて、Update Profiles のグラフが跳ねている場合は、SwiftUI 外が原因の可能性
    • Long View Body UpdatesViewbody プロパティが長時間実行されている場合を強調
    • Long Representable UpdatesViewViewControllerRepresentable の長時間更新を特定
    • Other Long Updates:その他の長時間 SwiftUI 更新を表示
  • 色分け:オレンジと赤で hitch や hang への寄与度を示す
    • まずは赤の更新箇所から確認することが出発点
  • 要件:Xcode 26 のインストールと最新 OS での SwiftUI traces サポート

4:20 – Diagnose and fix long view body updates

  • Command-I で リリースビルド+Instruments 起動、SwiftUI テンプレートを選択し、記録ボタンをクリック、アプリを操作
  • 問題の特定:トップレベルの 長い更新レーンを調査
    • Long View Body Updates のオレンジや赤に注目
  • Time Profiler での分析
    • View body 実行中の CPU 使用状況をコールスタックを展開して確認
    • 例:distance プロパティでの MeasurementFormatterNumberFormatter の重い処理を特定
  • レンダーループ
    • 正常な場合:イベント処理 → UI 更新 → フレーム期限前に完了 → レンダリング → 表示
    • hitch の場合:UI 更新が期限を超過 → 次のフレームが遅延 → 前フレームが長時間表示
  • パフォーマンスを高める上で、長くかかるビューの更新以外にも注意すべきこと
    • 更新が無駄に多い場合:多数の比較的高速な更新でもフレーム期限を逃す可能性

19:54 – Understand causes and effects of SwiftUI updates

  • SwiftUI の宣言的性質:UIKit のバックトレースとは異なり、SwiftUI では更新原因の特定が困難
  • AttributeGraph の動作
    • View protocol への準拠と body プロパティの実装
    • 親→子へ、属性 (attribute) を受け渡し、状態管理と依存関係の定義
      • ビュー構造体は頻繁に再作成されるが、属性がIDを維持し状態を維持する
    • state 変数変更時のトランザクション作成と期限切れのマーキング、依存関係チェーンで期限切れの更新伝播
    • 期限切れの依存関係がないものから更新を開始し、依存関係を追って逐次的に更新
  • 「なぜビュー本体が実行されたのか」→「何がビュー本体を期限切れとマークしたのか」を理解する → Cause & Effect Graph(原因と結果グラフ)
  • Cause & Effect Graph
    • 更新の原因と効果の関係をグラフで視覚化
    • 青いノード:自分のコードやユーザーアクション
    • 矢印:update や creation の関係を表示
  • ビューのデータ依存関係を細分化し、必要な箇所のみが更新されるようにするべき
    • 例:ビューがコレクションの表示する配列すべてへの依存関係を持つのではなく、ビューごとに @Observable な ViewModel を持たせる
  • Environment の考慮事項
    • EnvironmentValues 構造体への依存による更新伝播
    • 頻繁に変更される値(geometrytimer など)の environment 保存を避ける

35:01 – Next steps

  • ベストプラクティス
    • View body を高速に保つ
    • 不要な View body 更新の排除
    • データフローの設計で必要時のみ更新
    • 頻繁に変更される依存関係への注意
    • 開発中の定期的な Instruments 使用
  • 重要なポイントView body が高速かつ必要時のみに更新されることを保証する

Liquid Glass の展開する検索タブを試してみた

iOS 26 では、コンテンツを分けるタブ群と検索タブとが分離される。これにより、コンテンツ操作時と検索時とで、タブバー領域の見た目が明確に区別される作りとなっている。

実装は簡単。

// 検索画面
struct SearchTabView: View {
    @State private var text: String = ""
    
    var body: some View {
        NavigationStack {
            VStack {
                Image(systemName: "magnifyingglass")
                    .imageScale(.large)
                    .foregroundStyle(.tint)
                Text("Search")
            }
            .padding()
        }
        .searchable(text: $text)
        .tabViewSearchActivation(.automatic)
    }
}

...

// タブ定義
struct ContentView: View {
    var body: some View {
        TabView {
            Tab("Entries", systemImage: "doc.text") {
                EntriesTabView()
            }
            Tab("Answers", systemImage: "sparkles") {
                AnswerTabView()
            }
            Tab(role: .search) {
                SearchTabView()
            }
        }
    }
}

検索タブを選択した際に、検索フィールドにフォーカスを当てるか否かは、tabViewSearchActivation modifier で指定する。パラメタの TabSearchActivation はふたつあり、それぞれ挙動が異なる。

tabViewSearchActivation(_:) | Apple Developer Documentation
TabSearchActivation | Apple Developer Documentation

.automatic

検索タブをタップした際に、フィールドは展開するがフォーカスが当たるかは自動的に決まる。挙動を見るに、初期状態ではフォーカスは当たらず、フォーカスを当てたまま別タブに移動し、再度検索タブに戻った際にはフォーカス状態が復元されるように見える。

Music アプリがこの挙動をしており、検索画面として検索操作の前にあらゆる動線(カテゴリ)を見せたい場合に有効そう。

.searchTabSelection

検索タブをタップすると即フィールドが展開し、フィールドを閉じる(フィールド右のバツをタップする)と直前のタブに戻る。

Photos アプリがこの挙動をしている。前出の Music アプリと比較すると、以下の違いが見出せ、使い分けの参考にできそう。

  • Music が世界中の膨大なコンテンツから、検索キーワードだけでなく、ジャンル、アーティストといった軸をもとに探し出す体験
  • Photos はキーワードをもとに写真を検索する体験

ちなみに、検索タブの選択状態とカーソルフォーカスとは必ずしも連動しないため、検索タブを表示したままキーボードを閉じることはできる。(Photos ではこの状態で検索履歴の選択が行えるようになっている)


この検索タブの作りは、ミニマリズムでクリーンな印象を受ける一方で、上述したUIの仕組みやナビゲーションの構造を理解できていないと、やや使いこなすのが難しい気がしている。なぜなら iOS 18 以前は、タブ群はグローバルナビゲーションとして基本的に常時表示され、常に選択可能な状態であった。一方、iOS 26 では検索タブに遷移すると、タブ群はひとつのアイコンに集約され、選択肢が視認できなくなるからだ。

ユーザーが検索タブから任意のタブに移動したい場合、「メインのタブ群を展開する」→「目的のタブを選択する」という、2段階の操作を意識しなくてはいけない。ぼーっと触っているとこれが安易に頭から抜け、一瞬迷子になってしまうと感じている。

あっという間に9月到来

ブログにアウトプットできるネタがないので3分の2が過ぎた2025年これまでのことを振り返り。

日記を振り返ると2025年の目標は、ここ数年で積み上げた習慣が結果に結びつくようにし、環境や仕事を言いわけにやり残してきたことをやる年と決めて、こんなことができたら良いなと考えていたようだ。

  • 2024年の転職を機に始めた勉強会参加継続(月5回ペース)
  • これまでしてこなかった勉強会への登壇(理想は年5本)
  • アプリリリース(理想は3つ)
  • ビッグバンドへの参加
  • 楽器毎日練習、毎週どこかしらでセッション参加

実態はこんな感じ、、

👍勉強会参加継続
継続中

👍勉強会登壇
すでに2本、10月に1本予定(理想まであと2本)
登壇メモ:集まれSwift好き!Swift愛好会スピンオフ WWDC25セッション要約会 @ DeNA
登壇メモ:potatotips #92 iOS/Android開発Tips共有会

😭アプリリリース
ひとつもできていない

👍ビッグバンド参加
できた:自分語り:ビッグバンドに入団した話

😑楽器毎日練習、毎週セッション
暑くなって色々さぼりがち

あと4ヶ月、まだ間に合うか、、!?


とはいえ、年始段階ではあまり想定していなかったことができているので、トントンといったところか。

今 Foundation Models で試行錯誤している結果が、冬が来るまでに出せれば良いかなと思っている。そして、せめて1本のアプリリリースに繋げられたら、、
(本当は Vision Pro 使った開発も構想ばかりしているが、手をつけられていない)

あと、せっかく業務での Android 開発にも慣れて来たので、年内何かしら Android で学んできたことを発信できれば。

音楽は、、練習頻度下がりがちだが演奏は褒められることが増えてきたので、効率的に身になる練習ができていると信じたい。先生に 4-way とかルーディメンツ練習しろと言われているので、教本は買い足した(4-Way Coordination)。この辺をルーチン化するようにしたい。