View in English

  • メニューを開く メニューを閉じる
  • Apple Developer
検索
検索を終了
  • Apple Developer
  • ニュース
  • 見つける
  • デザイン
  • 開発
  • 配信
  • サポート
  • アカウント
次の内容に検索結果を絞り込む

クイックリンク

5 クイックリンク

ビデオ

メニューを開く メニューを閉じる
  • コレクション
  • トピック
  • すべてのビデオ
  • 利用方法

WWDC25に戻る

  • 概要
  • トランスクリプト
  • コード
  • InstrumentsによるCPUのパフォーマンス最適化

    Instrumentsの2つの新しいハードウェア支援ツールを使用して、Appleシリコン向けにアプリを最適化する方法を学びましょう。最初にアプリのプロファイリング方法を説明したあと、Processor Traceで呼び出される各関数を確認しつつ詳細を解説します。また、CPUカウンタの各種モードを使用したコード分析によりCPUのボトルネックを特定する方法についても説明します。

    関連する章

    • 0:00 - イントロダクションとアジェンダ
    • 2:28 - パフォーマンスに対するマインドセット
    • 8:50 - プロファイラ
    • 13:20 - Span
    • 14:05 - Processor Trace
    • 19:51 - ボトルネック分析
    • 31:33 - まとめ
    • 32:13 - 次のステップ

    リソース

    • Analyzing CPU usage with the Processor Trace instrument
    • Apple Silicon CPU Optimization Guide Version 4
    • Performance and metrics
    • Tuning your code’s performance for Apple silicon
      • HDビデオ
      • SDビデオ

    関連ビデオ

    WWDC25

    • アプリの電力消費のプロファイリングと最適化
    • InstrumentsによるSwiftUIのパフォーマンス最適化
    • Swiftによるメモリ使用量とパフォーマンスの向上

    WWDC24

    • Swiftのパフォーマンスの詳細

    WWDC23

    • Instrumentsによるハング分析

    WWDC22

    • Swiftの並行処理を視覚化して最適化する
  • このビデオを検索

    こんにちは OSカーネルエンジニアのMattです Instrumentsを使ってAppleシリコンCPU 向けにコードを最適化する方法を説明します CPUリソースを効率的に使うことで アプリで大量のデータを処理する場合や 迅速な応答が求められる場合に 待ち時間を短縮できます しかしソフトウェアのパフォーマンスの 予測は2つの理由から困難です 1つ目は Swiftのソースコードと 実際に実行される環境の間に 抽象化レイヤーがあることです アプリ用に記述したソースコードは 機械語の命令にコンパイルされ 最終的にCPU上で実行されます ただし コードは単独では実行されません コンパイラで生成された サポートコード、Swiftランタイム、 その他のシステム フレームワークによって拡張され アプリの代わりにカーネルの システムコールが特権的操作を処理します

    このため コードが依存するソフトウェア 抽象化のコストは把握しづらくなります コードのパフォーマンスを 予測するのが難しい2つ目の理由は CPUが与えられた命令を 実行する方法です 単一のCPU内では各機能単位が 並行して動作します 命令を効率的に実行するためです これに対応できるように 命令は順不同で実行されます 順番に実行されているように見えるだけです さらにCPUはいくつかの層からなる メモリキャッシュの恩恵を受けます データに迅速にアクセスできるのです これらの特性によってコーディングの 一般的なパターンは劇的に高速化されます 例えば メモリの線形スキャンや まれな条件下で早期退出する 防御的チェックなどがこれにあたります ただし一部のデータ構造、 アルゴリズム、実装アプローチは 慎重に最適化するか 大幅に再構成しないと CPUが効率的に実行できません CPU向けにコードを最適化するための 正しい方法を説明します 最初に パフォーマンスの 調査方法を確認しましょう まずはデータを指針とした 最大限の高速化に注目してみます 次に 従来のプロファイリング用 アプローチを検討します これは コード内の過剰なCPU使用率を 特定する良いきっかけとなります さらに掘り下げてプロファイリングの ギャップを埋めるため Processor Traceを使用して すべての命令を記録し ソフトウェア抽象化のコストを測定します 最後に 改良されたCPU CountersでCPUの ボトルネックを分析し アルゴリズムを 詳細に最適化する方法を学びます まずはパフォーマンス調査に アプローチするための 正しい考え方を知りましょう 大切なのは 先入観を持たないことです 減速の原因は予想外で驚くことがあります 前提条件をテストするための データを収集し コードの実行に関する メンタルモデルが 正確かどうかを検証します

    例えば シングルスレッドの CPUパフォーマンスだけでなく 他にも原因があるかもしれません CPUでの実行とは別の問題として スレッドやタスクも ファイルなどのリソースや共有可変状態に すぐにアクセスできず 待機状態となる場合があります 「Visualize and optimize Swift concurrency」セッションで タスクが実行待ちになる理由を 把握するためのツールを紹介しています

    スレッドのブロックが解除され 実行状態になった場合も 不適切な品質のサービスクラスが コードに適用されたり 暗黙的に作成するスレッド数が多すぎるなど APIが誤って使用されている 可能性があります 詳しくは コードのパフォーマンスの チューニングに関する文書をご覧ください しかし効率に問題がある場合は アルゴリズムとそれに関連する データ構造を変更するか または そのアルゴリズムをプログラミング言語で 実装する方法を見直す必要があります ツールを使って まず このツリーの どの部分に焦点を当てるべきかを判断します Xcodeの内蔵CPUゲージを使って アプリの操作中における CPUの使用率を確認してみましょう スレッド間のブロックの動作と 最終的にどのスレッドがブロックを 解除するかを分析するには System Traceを使用します

    UIまたはアプリのメインスレッドに 影響を与える問題については 専用のHangsを使用してください アプリのCPU使用率の最適化が 必要かどうかを確認する方法の 詳細については「Analyze hangs with Instruments」をご覧ください ただし ツールからのガイダンスがあっても 実装する最適化の種類には 注意してください むやみなマイクロ最適化は コードの拡張や推論を 難しくするかもしれません また多くの場合 コンパイラ最適化に 依存していて自動ベクトル化や 参照カウントの省略のように 脆弱な場合があります 煩わしいマイクロ最適化に取り組む前に 代替手段を探してください これにより 運用の低速化を 完全に回避できるかもしれません そもそも なぜそのコードを 実行するのかを考えてみましょう 実行する必要がなければ コードを削除するだけです ご視聴ありがとうございました… いえ冗談です 実際のところ これはまず不可能ですが その処理の結果が本当に重要かを 検討することができます

    また クリティカルパス外で 後で作業を試みたり 結果がユーザーに表示される場合に 限定する方法もあります 同様に 値を事前計算することで 作業の完了にかかる時間を 短く見せることができます ビルド時に値をベイクする方法も これに含まれます しかしこれらのアプローチは 不必要に電力を消費したり サイズを大きくしたりする場合があります 同じ入力内容で繰り返し操作を 行うのならキャッシュも有効です しかしこれも 多くの場合 一連の困難な問題が伴います キャッシュの無効化や メモリ使用量の増加などです パフォーマンスの問題があって その処理をどうしても 回避できない場合は CPUでの実行速度を 向上させる必要があります それが今日のセッションの主眼です ユーザー体験への影響が最も大きいコードを 優先して最適化してください 通常これは アプリを操作するユーザーの クリティカルパスに関与するコードです パフォーマンスの問題が 表面化しやすいと同時に 実行が長時間にわたり 多くの電力を消費する可能性もあります このセッションでは 準備済みの 整数のリストの検索に注目します 私のアプリのクリティカルパスだからです

    このアプリはバイナリ検索を使っています ソートされた配列を使う古典的な アルゴリズムで 検索範囲を 半分にし続けて要素を見つけます この例では 配列に16個の要素があり 数字の5を保持する要素を 検索しようとしています 5は配列の中央にある要素 20より小さい数です つまり 5という要素は前半にあるはずです 5は配列の前半にある要素 9より小さい数です つまり配列の最初の 4分の1のどこかにあるはずです 3と比較すると一致する数に たどり着くので わずか4ステップです これは私のアプリで使っている フレームワークでのバイナリ検索の実装です このスタンドアロン関数では 干し草の山から針を見つけるという 言い回しに由来する パラメータ名を付けています haystackコレクションから Comparableとなるneedleを検索できます ここでは2つの変数を追跡します startでは現在の検索エリアの開始を lengthでは検索対象の 要素の残数を追跡します 検索対象の要素が残っている場合は 検索範囲の中央の値を確認します needleがその値より小さい場合は 検索範囲を半分にするだけです startはそのまま残します needleが値と等しい場合は 要素が見つかっているので middleインデックスが返されます それ以外の場合は開始位置を 調整する必要があります 中央の要素の直後に移し 検索範囲を半分に減らしました

    このアルゴリズムを段階的に 最適化するための準備をします 検索のスループットや アルゴリズムが 毎秒完了可能な 検索の数を比較しながら 各ステップで進捗を確認します 変更を加えるたびに 大きな改善が見られなくても構いません 一つひとつは定量化が難しくても 積もり積もって大きな改善につながります

    継続的に最適化できるように 検索スループットを測定する 自動テストを作成しました パフォーマンスを見積もるだけなので 難しいセットアップは不要です このrepeat-whileループにより 指定された期間が 経過するまで検索クロージャを呼び出します 検索クロージャの呼び出しでは OSSignposter intervalを使用しています 最適化テストの部分を ツールで絞り込めるようにするためです .pointsOfInterestカテゴリを選択しました Instrumentsには既定で含まれています タイミング自体にはContinuousClockを 使用します Dateとは異なり 逆行できずオーバーヘッドが小さいです このように単純かつ効果的な方法で アルゴリズムのパフォーマンスの 大まかなデータを収集できます searchCollectionというテストでアプリでの バイナリ検索をシミュレートします 1回の記録で複数の テストを実行する場合は signpostに付けた説明的な名前で 1秒間検索を実行します クロージャ内のループは binarySearch関数を呼び出して 時間を確認するコストを償却します このテストを Instrumentsプロファイラで実行して バイナリ検索のCPUパフォーマンスを 分析してみましょう CPUに重点を置いたプロファイラは2つあり Time Profilerと CPU Profilerから選択できます 従来のTime Profilerは システムのCPU上で実行される処理を タイマーに基づいて 定期的にサンプリングします この例では 2つのCPUで 処理が実行されています Time Profilerは CPU上で実行される 各スレッドのユーザー空間コールスタックを 各サンプルポイントでキャプチャします

    Instrumentsはこれらのサンプルを コールツリーまたはフレームグラフとして 詳細ビューで視覚化し CPUの パフォーマンス改善のためには どのコードを最適化するべきか おおむね予測します これは時間経過に伴う処理の分散や 同時にアクティブになる スレッドを探すのに役立ちます このようにタイマーでサンプリングすると エイリアシングが問題になります エイリアシングとは システム上の一部の 定期処理がサンプリングタイマーと 同じ頻度で発生することです この例では 青い領域が CPU時間のほとんどを占めていますが サンプラがコールスタックを収集するたびに オレンジ色の関数が実行されています その結果 Instrumentsのコールツリーには オレンジ色が不当に多く表示されます

    この問題を回避するために CPU Profilerを利用できます これは各CPUのクロック周波数に基づいて CPUを個別にサンプリングします CPUの最適化はTime Profilerよりも CPU Profilerを優先しましょう CPUリソースを消費するソフトウェアを より正確かつ公平に重み付けしてくれます

    ベルはCPUのサイクルカウンタが サンプリングするタイミングを表しています AppleシリコンのCPUは非対称で 一部はクロック周波数がやや遅いですが そのぶん電力効率に優れています 周波数をスケールアップした個々のCPUは より頻繁にサンプリングされます Time Profilerのような 高速実行の CPUに対するバイアスはありません CPU Profilerを使って binarySearch関数の中でCPUサイクルを 最も消費している部分を調べましょう XcodeのTest Navigatorで ユニットテストからInstrumentsを 素早く起動できます テスト名を副ボタンでクリックして プロファイル項目を選択するだけです 今回は「Profile searchCollection」 を選択します

    Instrumentsが開き テンプレート選択画面が表示されます を選択します で モードに切り替えて オーバーヘッドを減らし記録を開始します プロファイラのデフォルトの 即時モードはアプリをキャプチャしながら 操作を確認するのに役立ちます しかしInstrumentsと同じマシンで 自動テストを行う場合は ツールによるオーバーヘッドの増大は 最小限に抑えたいところです 記録が停止するまで待ってから分析します Instrumentsの新しいドキュメントは しばしば煩雑です ウィンドウが2つに分かれています 上部のトラックはタイムライン上の アクティビティを示しています 各トラックに複数のレーンを追加して レベルや領域を示すグラフを表示できます

    タイムラインの下の詳細ビューには 調査対象の タイムライン範囲に関する サマリ情報が表示されます 右側にはさらに詳しい情報が表示されます 方向付けのために トラックで検索が行われている 領域を見つけます 副ボタンで領域をクリックすると 調査範囲の設定が提案され 下の詳細ビューはsignpost間隔内での キャプチャデータのみに絞り込まれます テストランナープロセスの トラックをクリックすると タイムラインの下の詳細ビューに CPUプロファイルが表示されます ここには 各CPUのサイクルカウンタに よってサンプリングされた テスト内の関数の コールツリーが表示されます オプションを押しながら一覧の最初の関数の 横にある下向き矢印をクリックすると サンプル数が大きく異なる最初の ポイントまでツリーが展開します これはbinarySearch関数に近いです 名前の横にある矢印をクリックして binarySearch関数に焦点を当て を選択します 各関数は サンプル数に 各サンプル間のサイクル数を 掛けた数値で重み付けされます このコールツリーはCollection型に応じて バイナリ検索によって呼び出された関数で 取得された多くのサンプルを示しています このprotocol witnessは サンプルの約4分の1に出現しています 割り当てのほかObjective-C型の Arrayチェックもあります いま私が検索している型のデータに よく一致するコンテナタイプに 切り替えると Arrayと ジェネリクスのオーバーヘッドを 回避できます 新しいSpan型を試しましょう 要素がメモリに連続格納されている場合 Collectionの代わりに使用できます これは多くの種類の データ構造に共通しています 事実上はベースアドレスとカウントです それが使用されている関数の 外部でのメモリ参照の エスケープやリークも防止されます Spanの詳細については 「Improve memory usage and performance with Swift」をご覧ください Spanを採用するにはhaystackと 戻り値の型をSpanに変更するだけでよく アルゴリズム自体の変更は不要です

    この小規模な変更だけで 検索が4倍速くなります しかし このバージョンのバイナリ検索は まだアプリに影響を与えているので Spanの境界チェックがオーバーヘッドに 影響しているかどうかを 調査したいと思います Processor Traceという 新しいツールでさらに掘り下げます Instruments 16.3以降では Processor Traceを使用して アプリのプロセスがユーザー空間で実行する すべての命令のトレースを収集できます ソフトウェアパフォーマンスの革新的な 測定方法であり サンプリングバイアスが 発生しません アプリのパフォーマンスへの影響は たった1%なので無視できます Processor Traceには M4搭載のMacとiPad Proまたは A18搭載のiPhoneでのみ利用できる 専用のCPU機能が必要です まず Processor Trace用に デバイスを設定する必要があります Macで> の設定をオンにします iPhoneまたはiPadの場合 この設定は セクションにあります Processor Traceで最大限に エクスペリエンスを引き出すには トレースを数秒だけに制限してみてください CPU Profilerによるサンプリングとは 異なり 作業のバッチ処理が不要です 最適化したいコードの インスタンス1つだけでも十分です Spanバージョンのバイナリ検索で Processor Traceを実行してみましょう あとは何回か反復処理をして テストを実行するだけです このテストをプロファイリングするには 行番号のガター部分にあるテストアイコンを 副ボタンでクリックします 前に使用した メニューが表示されますが ナビゲータを 切り替えるより簡単です Processor Traceテンプレートを選択して

    記録を開始します

    Processor Traceは大量のデータを 扱う必要があるので キャプチャと分析に 時間がかかる場合があります Processor Traceはすべての分岐判断を 記録するようにCPUを設定します サイクル数と現在時刻も記録され 各関数に費やされた CPUの使用時間を追跡します その後Instrumentsはアプリとシステム フレームワークの実行可能バイナリを 使用して実行パスを再構築し 関数呼び出しに 経過したサイクルと期間の注釈を付けます トレースに費やす時間は制限します CPUが記録する情報は 最小限に抑えられているとはいえ マルチスレッドのアプリでは 毎秒数ギガバイトの データになる可能性があります ドキュメントの準備ができたので binarySearch関数の呼び出しを 詳しく調べてみましょう 検索は 今や記録全体の ほんの一部しか占めていないので タイムラインの下の詳細ビューの リストで探します この行を副ボタンでクリックし を選択します バイナリ検索を実行している スレッドを見つけるには セルを副ボタンでクリックし を選択します

    Processor Traceは各スレッドトラックに フレームグラフという新関数を追加します ピンの区切り線を上にドラッグして スペースを確保しますね

    Processor Traceは実行を フレームグラフとして視覚的に示します フレームグラフは関数のコストと関係を グラフィカルに表現したものです バーの幅は関数の実行に かかった時間を表し 行はネストされたコールスタックを表します しかし ほとんどのフレームグラフは サンプリングのデータを示しており そのコストはサンプル数に基づく推定値です 一方 Processor Traceの タイムラインフレームグラフでは 経時的な呼び出し記録が表示されます CPUで実行されたタイミングと まったく同じです 各バーの色は 由来しているバイナリの種類を表します システムフレームワークは茶色 マゼンタはSwiftランタイムと 標準ライブラリ 青は任意のカスタムフレームワークか アプリのバイナリにコンパイルされたコード このトレースの最初の部分はsignpostを 出力するオーバーヘッドを示します では 範囲の終わり近くにある バイナリ検索コードを拡大してみましょう Optionキーを押しながらタイムラインを クリックし ドラッグして拡大します

    10回の反復処理から任意の binarySearch関数呼び出しを選択できます 調査範囲を設定して 副ボタンのクリックでズームします これがProcessor Traceの力です 数百ナノ秒しか実行されない 1つの関数によって行われた すべての呼び出しを確認できます さらに拡大することもできますが 今回はタイムラインの下にある 関数呼び出しの要約を使用します タイムラインと同じ情報が 表形式で示されます 短時間使用の関数まで含めて フルネームが表示されます この表をサイクル順に並べ替えます

    境界チェックが速度低下の原因だという 私の最初の仮定は誤りでした このバイナリ検索の実装はまだプロトコルの メタデータのオーバーヘッドの処理中です 数値比較のインライン展開はできず 検索の合計サイクル数の割合は 結局かなりのものになるでしょう これは汎用的なComparable パラメータが 使用される要素の型に 特化されていないためです

    このコードはアプリによって リンクされたフレームワークにあるので Swiftコンパイラはバイナリ検索の 呼び出し元によって渡された型専用の 特別なバージョンを提供できません

    これが原因でフレームワークのコードで オーバーヘッドが発生する場合 フレームワークの関数に inlinable注釈を追加して フレームワークのクライアントの バイナリ実行ファイルで 特別な実装を生成する必要があります

    しかし インライン展開すると 呼び出し元と 混ざり合うため コードの解析が難しくなります テストハーネスへのインライン展開は 避けたいので この関数では アプリやテスト用のInt型用として 手動で特化させて 新しい関数名を付けます コードは汎用性をかなり失いますが 約1.7倍高速になります さらに最適化を続けましょう まだバイナリ検索がアプリの速度低下を 引き起こしているからです 1つの関数の最適化に 長時間かかるのは奇妙な事態です 定期的に再評価してデータを収集し コードを非効率にしている 他の要因がないか探りましょう 特別なSpanバイナリ検索には Processor Traceの予期せぬ関数呼び出しも 表示されていないので コードがCPU上でどのように実行および 進行しているか把握する必要があります CPU Countersを使用すると CPUでの実行中 コードにどのような ボトルネックがあるかがわかります Instrumentsを再び使用する前に CPUの動作について メンタルモデルを構築する必要があります 基本的には CPUは命令の リストに従っているだけです レジスタとメモリの変更や 周辺機器との相互作用などがあります

    CPUが処理を実行する時は 一連の手順に従う必要があります これは大きく2つのフェーズに分類されます 1つ目は 実行するべき命令を CPUに届けるための命令送信 2つ目は それらを実行する命令処理です 命令送信では命令がフェッチされ マイクロ操作にデコードされて CPUが実行しやすい形になります ほとんどの命令は単一の マイクロ操作にデコードされますが メモリリクエストの発行やインデックス値の インクリメントなど複数の場合もあります マイクロ操作は Map and Scheduleユニットに送信されて ルーティングとディスパッチが行われます その後実行ユニットに割り当てられます 操作がメモリにアクセスする必要があれば Load-Storeユニットに割り当てられます

    これらのフェーズを順番に実行する場合 CPUがフェッチを再開するまでに 時間がかかるので Appleシリコン プロセッサはパイプライン化されます ユニットは処理を完了すると 次の操作に移るので どれも常にビジーです

    実行ユニットのパイプライン化と 追加コピーの作成は 命令レベルの並列処理を容易にします

    これはプロセスまたはスレッドレベルの 並列処理とは異なります Swiftの並行処理または Grand Central Dispatchで複数のCPUが 異なるOSスレッドを実行しているところへ アクセスするのではなく 命令レベルの並列処理では 1つのCPUが時間的に有利を得ます ユニットがアイドル状態の時に パイプラインすべてをビジー状態に保ち ハードウェアを効率的に使用できるからです Swiftソースコードは この並列処理を 直接制御することはできません 代わりに コンパイラによる従順な 命令シーケンスの生成を支援します

    残念ながら 並列化可能な命令シーケンスは CPU内のユニット間の やり取りの性質上 あまり直感的ではありません ユニット間の矢印は 並列処理が 制限され パイプラインの操作が 停止する可能性を示しています これをボトルネックと呼びます

    ワークロードに関連する ボトルネックを見つけるには AppleシリコンCPUで 各ユニットの特徴的なイベントや 実行される命令の その他の特性をカウントします CPU Countersは これらのカウンタを読み取り より高いレベルのメトリックを構築します 今年はカウンタにプリセットモードが 追加されて非常に使いやすくなりました Instrumentsはこれらを ガイド付きの反復的手法で使用し コードのパフォーマンスを ボトルネック分析します これを使って 関数呼び出しに明らかな オーバーヘッドがないにもかかわらず バイナリ検索がまだ遅い理由を 調べてみましょう CPU Countersはワークロードの サンプリングに依存するため CPU Profilerで使ったテストハーネスに戻り スループットを再測定する必要があります

    特別なSpan実装のテストを Instrumentsでプロファイリングします

    テンプレートを選択します

    厳選されたモードで使える ガイド付きの構成を使って測定できます

    各モードの機能に興味がおありの場合は モード選択の横にある情報アイコンから 利用可能なドキュメントをご参照ください カウントを始めましょう

    この最初のCPU Bottlenecksモードでは CPUが実行した処理が分析され CPUの潜在的なパフォーマンスを占める 4つの主なカテゴリが表示されます Instrumentsはこれらを色付きの 積み重ね横棒グラフとして表示します 詳細ビューには概要テーブルもあります 記録中 Instrumentsはテストで使用した スレッドのCPUカウンタデータを収集し それらをボトルネックの割合に変換します 前と同じようにPoints of Interestを 使って指針を定め ズームして検索を選択します

    次に バイナリ検索の実装を実行している スレッドをタイムラインにピン留めします

    レーンにカーソルを 置くと 破棄されたボトルネックの 割合が高いことがわかります 下の詳細ビューは調査範囲内の メトリックの集計を示しています の行を選択すると 右側の詳細ビューに 説明が表示されます また Instrumentsではタイムラインの チャート上にコメントが表示されます そのコメントをクリックすると さらなる詳細が下に表示されます これは便利ですが 検索のどの部分が ボトルネックの原因かはまだわかりません セルを 副ボタンでクリックします 列の下です ここでは ワークロードを様々なモードで 再プロファイリングできます 試してみましょう このモードは CPUボトルネックとは少し異なります 引き続きカウンタのデータを収集しますが サンプリングをトリガーするための カウンタの設定もしています サンプルデータは 破棄された処理を生成する命令に 限定されています 再びで そのことを確認しましょう

    次にテストプロセスのトラックを選択し

    タイムラインの下にある 命令サンプルに移動します

    これはコールスタックではなく 実際に問題の原因となっている命令です 関数名の横にある矢印をクリックすると サンプリングされたソースコードが ソースビューアに表示されます CPUが間違った分岐方向に 向かったので確認します ここではneedleと中間値の間で 行われている比較が誤って予測されています これらのソースラインが不正確な予測を 多発させている理由を理解するには CPUの特徴をもう少し知る必要があります

    実は CPUは命令を順不同で実行します 命令が完了した後で並べ替えているため 命令が順番に実行されるように 見えるだけです これはCPUが先を見て 次に実行する命令を 予測していることを意味します 分岐予測機能は通常は正確ですが 以前の実行に 一貫したパターンがない場合は 誤った経路をたどる可能性もあります

    今回のバイナリ検索アルゴリズムの ループには2種類の分岐があります 最初のループ条件は 通常は ループの終わりまで適用されます これは予測に成功しており サンプリングには現れませんでした しかしneedleのチェックは 事実上ランダムな分岐なので 予測に問題が発生したのも 不思議ではありません

    制御フローに影響を与える分岐の 予測が難しくならないように ループ本体を書き直します ifステートメントの本文は 条件に基づいて値を代入しているだけです これでSwiftコンパイラは条件付きの 移動命令を生成することで 別の命令への分岐を回避できます 関数から戻ることや 条件に基づいてループを中断することは 分岐で実装する必要があるため 早期リターンも削除する必要がありました 分岐先でプログラムが終了しないように 未チェックの算術演算も使用しました これはマイクロ最適化が脆弱化して 停止しやすい領域の1つであり 安全性とわかりやすさが 低いことは言うまでもありません このような変更を行う場合は 最初のCPU Bottlenecksモードに戻って 残りのボトルネックに どのように影響するかを確認します 今回は既に 分岐のない 新しいバイナリ検索の痕跡を集めました 分岐のあるバージョンの約2倍の速さです 現在はほぼ完全に命令処理の ボトルネックになっています Instrumentsは ワークロードを Instruction Processingモードで 再実行する必要性を示しています

    このモードにはL1D Cache Miss Sampling モードの実行を推奨するコメントがありました キャッシュミスのサンプルは 配列からメモリへのアクセスが原因で CPUが命令を効率的に 実行できないことを示しています その理由を知るため CPUとメモリについて詳しく学びましょう

    CPUは 同じアドレスに繰り返し アクセスするキャッシュ階層 または 予測可能なアクセスパターンでメモリに アクセスすることで 高速化を図ります これは各CPU内にある L1キャッシュから始まります あまり多くのデータは保存できませんが メモリへのアクセスが最速です L2キャッシュはより低速ですがCPUの 外にあり ヘッドルームが大幅に増加します 最後に どちらのキャッシュも使わずに メインメモリにアクセスするリクエストは 早いパスと比較して50倍遅くなります キャッシュはメモリを64または128バイトの キャッシュラインにグループ化します 命令が4バイトしか要求しない場合でも キャッシュはより多くのデータをプルします 後続の命令で近くのバイトにも アクセスする必要性を 予期するからです

    これがバイナリ検索アルゴリズムに どのように影響するか考えてみましょう この例の青い線は 配列内の要素です 灰色のカプセルはCPUキャッシュが 動作するキャッシュラインです

    配列は完全にキャッシュから開始します 最初の比較ではキャッシュラインと数個の 要素がL1データキャッシュに取り込まれます しかし次の比較では キャッシュミスが発生します その後の反復処理でも キャッシュミスが続きます これはキャッシュラインサイズの領域上で 検索が絞り込まれるまで続きます バイナリ検索は CPUのメモリ階層にとっては 異常なケースだとわかるでしょう

    要素を並べ替えて キャッシュしやすくすることを 許容できるなら 検索ポイントを 同じキャッシュラインに配置できます これはこのように家系図を整理した16世紀の オーストリアの系図学者にちなんで Eytzingerレイアウトと呼ばれています これは軽微な結果を期待する 一般的な最適化ではありません 検索速度は向上しますが 順序に沿ったトラバース速度が低下し その操作はキャッシュミスされます バイナリ検索の最初の例に戻り ソート済みの配列をEytzingerレイアウトに 並べ替える方法を示しましょう 中央の要素をルートとして開始し バイナリ検索操作を ツリーとしてモデル化します 中間点は子孫ノードです Eytzingerレイアウトはそのツリーの 幅優先トラバースとして配置されます

    ツリーのルートに近い要素は より密に配置され キャッシュラインを共有しやすくなります 5を再度検索すると最初の3ステップが 同じキャッシュラインで実施されます リーフノードは 配列の最後にソートされるので キャッシュミスが避けられません

    Eytzingerバイナリ検索の CPUボトルネックトレースを記録しました これもまた 分岐のない検索より 2倍高速であることを示しています しかしこの例では 興味深いことが強調されています 命令処理上 まだ技術的な ボトルネックであるということです 実装をキャッシュしやすくしましたが ワークロードはまだ本質的に メモリに縛られています

    パフォーマンスを監視し アプリの他のコードを 停止/最適化するタイミングを 把握する必要があります この検索は もはやクリティカルパスの パフォーマンスに影響しないからです このプロセスにより 検索スループットが大幅に向上しました まずCPU Profilerを使用してCollectionから Spanへの切り替えを 大幅に高速化しました

    次にProcessor Traceで 全般的なオーバーヘッドを確認しました 最後に ボトルネック分析を 活用したマイクロ最適化で パフォーマンスを大幅に向上させました Instrumentsにより 検索機能は 全体で約25倍高速化しました これらの高速化を達成するために 正しいマインドセットを確認しました ツールを使って推測を確認し 抽象化のコストについての 直感を養うことができました 続いて より詳細なツールを適用して 予期せぬオーバーヘッドを見つけ出しました 実際に測定しないと 見落としやすいですが 対処も簡単でした その後 ソフトウェアの オーバーヘッドに対処し CPUボトルネックに焦点を当てた 最適化を検討しました CPUで利用できる機能をより深く意識し 寄り添えるようになりました この順序は重要でした CPU重視のツールと ソフトウェアランタイムの 余分なオーバーヘッドを 混同してはならないからです

    これを自分のアプリに適用するには データを収集し パフォーマンスの マインドセットに従います パフォーマンステストはInstrumentsで 繰り返し測定できるように記述します これらのツールに関するご意見やご質問を フォーラムにお寄せください ここで紹介したセッションとWWDC24の Swiftパフォーマンスのセッションは Swiftの強力な抽象化のコストに関する より正確なメンタルモデルの 構築に役立ちます CPUがコードを実行する方法について 理解を深めるには 「Apple Silicon CPU Optimization Guide」をご覧ください ありがとうございました Instrumentsを活用し コードという「干し草の山」から 最適化の「針」を見つけてください

    • 6:37 - Binary search in Collection

      public func binarySearch<E, C>(
          needle: E,
          haystack: C
      ) -> C.Index where E: Comparable, C: Collection<E> {
          var start = haystack.startIndex
          var length = haystack.count
      
          while length > 0 {
              let half = length / 2
              let middle = haystack.index(start, offsetBy: half)
              let middleValue = haystack[middle]
              if needle < middleValue {
                  length = half
              } else if needle == middleValue {
                  return middle
              } else {
                  start = haystack.index(after: middle)
                  length -= half + 1
              }
          }
      
          return start
      }
    • 7:49 - Throughput benchmark

      import Testing
      import OSLog
      
      let signposter = OSSignposter(
          subsystem: "com.example.apple-samplecode.MyBinarySearch",
          category: .pointsOfInterest
      )
      
      func search(
          name: StaticString,
          duration: Duration,
          _ search: () -> Void
      ) {
          var now = ContinuousClock.now
          var outerIterations = 0
          
          let interval = signposter.beginInterval(name)
          let start = ContinuousClock.now
          repeat {
              search()
              outerIterations += 1
              now = .now
          } while (start.duration(to: now) < duration)
          let elapsed = start.duration(to: now)
          let seconds = Double(elapsed.components.seconds) +
                  Double(elapsed.components.attoseconds) / 1e18
          let throughput = Double(outerIterations) / seconds
          signposter.endInterval(name, interval, "\(throughput) ops/s")
          print("\(name): \(throughput) ops/s")
      }
      
      let arraySize = 8 << 20
      let arrayCount = arraySize / MemoryLayout<Int>.size
      let searchCount = 10_000
      
      struct MyBinarySearchTests {
          let sortedArray: [Int]
          let randomElements: [Int]
          
          init() {
              let sortedArray: [Int] = (0..<arrayCount).map { _ in
                      .random(in: 0..<arrayCount)
              }.sorted()
              self.randomElements = (0..<searchCount).map { _ in
                  sortedArray.randomElement()!
              }
              self.sortedArray = sortedArray
          }
      
          @Test func searchCollection() throws {
              search(name: "Collection", duration: .seconds(1)) {
                  for element in randomElements {
                      _ = binarySearch(needle: element, haystack: sortedArray)
                  }
              }
          }
      }
    • 13:46 - Binary search in Span

      public func binarySearch<E: Comparable>(
          needle: E,
          haystack: Span<E>
      ) -> Span<E>.Index {
          var start = haystack.indices.startIndex
          var length = haystack.count
      
          while length > 0 {
              let half = length / 2
              let middle = haystack.indices.index(start, offsetBy: half)
              let middleValue = haystack[middle]
              if needle < middleValue {
                  length = half
              } else if needle == middleValue {
                  return middle
              } else {
                  start = haystack.indices.index(after: middle)
                  length -= half + 1
              }
          }
      
          return start
      }
    • 15:09 - Throughput benchmark for binary search in Span

      extension MyBinarySearchTests {
          @Test func searchSpan() throws {
              let span = sortedArray.span
              search(name: "Span", duration: .seconds(1)) {
                  for element in randomElements {
                      _ = binarySearch(needle: element, haystack: span)
                  }
              }
          }
      
          @Test func searchSpanForProcessorTrace() throws {
              let span = sortedArray.span
              signposter.withIntervalSignpost("Span") {
                  for element in randomElements[0..<10] {
                      _ = binarySearch(needle: element, haystack: span)
                  }
              }
          }
      }
    • 19:17 - Binary search in Span

      public func binarySearchInt(
          needle: Int,
          haystack: Span<Int>
      ) -> Span<Int>.Index {
          var start = haystack.indices.startIndex
          var length = haystack.count
      
          while length > 0 {
              let half = length / 2
              let middle = haystack.indices.index(start, offsetBy: half)
              let middleValue = haystack[middle]
              if needle < middleValue {
                  length = half
              } else if needle == middleValue {
                  return middle
              } else {
                  start = haystack.indices.index(after: middle)
                  length -= half + 1
              }
          }
          return start
      }
    • 23:04 - Throughput benchmark for binary search in Span

      extension MyBinarySearchTests {
          @Test func searchSpanInt() throws {
              let span = sortedArray.span
              search(name: "Span<Int>", duration: .seconds(1)) {
                  for element in randomElements {
                      _ = binarySearchInt(needle: element, haystack: span)
                  }
              }
          }
      }
    • 26:34 - Branchless binary search

      public func binarySearchBranchless(
          needle: Int,
          haystack: Span<Int>
      ) -> Span<Int>.Index {
          var start = haystack.indices.startIndex
          var length = haystack.count
      
          while length > 0 {
              let remainder = length % 2
              length /= 2
              let middle = start &+ length
              let middleValue = haystack[middle]
              if needle > middleValue {
                  start = middle &+ remainder
              }
          }
      
          return start
      }
    • 27:20 - Throughput benchmark for branchless binary search

      extension MyBinarySearchTests {
          @Test func searchBranchless() throws {
              let span = sortedArray.span
              search(name: "Branchless", duration: .seconds(1)) {
                  for element in randomElements {
                      _ = binarySearchBranchless(needle: element, haystack: span)
                  }
              }
          }
      }
    • 29:27 - Eytzinger binary search

      public func binarySearchEytzinger(
          needle: Int,
          haystack: Span<Int>
      ) -> Span<Int>.Index {
          var start = haystack.indices.startIndex.advanced(by: 1)
          let length = haystack.count
      
          while start < length {
              let value = haystack[start]
              start *= 2
              if value < needle {
                  start += 1
              }
          }
          
          return start >> ((~start).trailingZeroBitCount + 1)
      }
    • 30:34 - Throughput benchmark for Eytzinger binary search

      struct MyBinarySearchEytzingerTests {
          let eytzingerArray: [Int]
          let randomElements: [Int]
      
          static func reorderEytzinger(_ input: [Int], array: inout [Int], sourceIndex: Int, resultIndex: Int) -> Int {
              var sourceIndex = sourceIndex
              if resultIndex < array.count {
                  sourceIndex = reorderEytzinger(input, array: &array, sourceIndex: sourceIndex, resultIndex: 2 * resultIndex)
                  array[resultIndex] = input[sourceIndex]
                  sourceIndex = reorderEytzinger(input, array: &array, sourceIndex: sourceIndex + 1, resultIndex: 2 * resultIndex + 1)
              }
              return sourceIndex
          }
      
          init() {
              let sortedArray: [Int] = (0..<arrayCount).map { _ in
                  .random(in: 0..<arrayCount)
              }.sorted()
              var eytzingerArray: [Int] = Array(repeating: 0, count: arrayCount + 1)
              _ = Self.reorderEytzinger(sortedArray, array: &eytzingerArray, sourceIndex: 0, resultIndex: 1)
              self.randomElements = (0..<searchCount).map { _ in
                  sortedArray.randomElement()!
              }
              self.eytzingerArray = eytzingerArray
          }
      
          @Test func searchEytzinger() throws {
              let span = eytzingerArray.span
              search(name: "Eytzinger", duration: .seconds(1)) {
                  for element in randomElements {
                      _ = binarySearchEytzinger(needle: element, haystack: span)
                  }
              }
          }
      }

Developer Footer

  • ビデオ
  • WWDC25
  • InstrumentsによるCPUのパフォーマンス最適化
  • メニューを開く メニューを閉じる
    • iOS
    • iPadOS
    • macOS
    • tvOS
    • visionOS
    • watchOS
    Open Menu Close Menu
    • Swift
    • SwiftUI
    • Swift Playground
    • TestFlight
    • Xcode
    • Xcode Cloud
    • SF Symbols
    メニューを開く メニューを閉じる
    • アクセシビリティ
    • アクセサリ
    • App Extension
    • App Store
    • オーディオとビデオ(英語)
    • 拡張現実
    • デザイン
    • 配信
    • 教育
    • フォント(英語)
    • ゲーム
    • ヘルスケアとフィットネス
    • アプリ内課金
    • ローカリゼーション
    • マップと位置情報
    • 機械学習とAI
    • オープンソース(英語)
    • セキュリティ
    • SafariとWeb(英語)
    メニューを開く メニューを閉じる
    • 英語ドキュメント(完全版)
    • 日本語ドキュメント(一部トピック)
    • チュートリアル
    • ダウンロード(英語)
    • フォーラム(英語)
    • ビデオ
    Open Menu Close Menu
    • サポートドキュメント
    • お問い合わせ
    • バグ報告
    • システム状況(英語)
    メニューを開く メニューを閉じる
    • Apple Developer
    • App Store Connect
    • Certificates, IDs, & Profiles(英語)
    • フィードバックアシスタント
    メニューを開く メニューを閉じる
    • Apple Developer Program
    • Apple Developer Enterprise Program
    • App Store Small Business Program
    • MFi Program(英語)
    • News Partner Program(英語)
    • Video Partner Program(英語)
    • セキュリティ報奨金プログラム(英語)
    • Security Research Device Program(英語)
    Open Menu Close Menu
    • Appleに相談
    • Apple Developer Center
    • App Store Awards(英語)
    • Apple Design Awards
    • Apple Developer Academy(英語)
    • WWDC
    Apple Developerアプリを入手する
    Copyright © 2025 Apple Inc. All rights reserved.
    利用規約 プライバシーポリシー 契約とガイドライン