Instruments を用いたCPUレベルでのパフォーマンス最適化のテクニック。バイナリサーチアルゴリズムの実装例をもとに、CPU内部の動作をマインドセットとして3ステップに分割し詳解。図解もあって直感的で、非常に見応えあったものの、後半にいくにつれ難解で理解を放棄してしまった。
0:00 – Introduction & Agenda
- パフォーマンス予測の困難さ:
- Swift ソースコードから実際の実行環境の間に抽象化レイヤーがある
- 機械語化されCPU実行
- ランタイム、サポートコード、フレームワーク、カーネルも含まれ抽象化レイヤーは把握しづらい
- CPU の並列実行、順不同(out-of-order)で実行、メモリキャッシュによる複雑性
- Swift ソースコードから実際の実行環境の間に抽象化レイヤーがある
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 を利用
- Time 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が時間的に有利を得る
- ユニット間でのやりとりにより、並列処理制限、パイプライン操作が停止される可能性:ボトルネック
- 命令送信(Instruction Delivery)
- ボトルネックの特定:
- 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倍の高速化を実現
- 段階的なアプローチ:
- CPU Profiler:Collection から Span への変更
- Processor Trace:unspecialized generics の overhead 発見
- Bottleneck Analysis:micro-optimization による大幅な性能向上
- 重要な順序:software overhead の解決 → CPU bottleneck の最適化
32:13 – Next steps
- 実践的なアプローチ:
- データ収集とパフォーマンスマインドセット
- 繰り返し測定可能なパフォーマンステストの作成
- Instruments の継続的な使用