> ## Documentation Index
> Fetch the complete documentation index at: https://docs.xbox.com/llms.txt
> Use this file to discover all available pages before exploring further.

# ハイブリッド CPU 環境でのゲーム パフォーマンス

> Intel、AMD、Qualcomm のヘテロジニアス CPU 向けに、PC ゲームのスレッド アフィニティ、CPU セットの列挙、優先度ブーストを調整して、一貫したフレーム時間を実現します。

CPU メーカーは、パフォーマンス、消費電力、ダイ面積のバランスを常に取っています。その理由は、顧客によって使用パターンや優先事項が異なるためです。デスクトップ マシンを使用するユーザーはパフォーマンスをより重視し、ノート PC のユーザーはバッテリー寿命をより重視します。ダイ面積が小さいほど、製造コストを抑えることができます。これらの相反する要件に対応するため、CPU メーカーは、コスト効率を維持しながら、システムが現在のワークロードの要求に動的に適応できる *ヘテロジニアス アーキテクチャ* を設計しています。

CPU メーカーは、効率レベルの異なる CPU コアを使用することで、ヘテロジニアス トポロジを実装しました。このアプローチでは、既存の CPU アーキテクチャの一部のコアを、より電力効率の高いコアに置き換えました。ユーザーが CPU のフル パフォーマンスを必要としない場合、OS は高性能なコアをオフにして大幅に電力を節約し、その結果バッテリー寿命が長くなります。より高いパフォーマンスが必要になると、OS は高性能なコアを動的にオンにして、増加したワークロードを処理できます。

## ハードウェア

独立系ハードウェア ベンダー (IHV) によって、ヘテロジニアス CPU トポロジの定義は異なります。Intel と Qualcomm は **Performance** と **Efficiency** という用語を使用し、AMD は **Performance** と **Compact** という用語を使用します。各ベンダーは、コア トポロジを設計する際に、パフォーマンスと効率のトレードオフを行っています。

次の例は、関連する設計上の選択の一部を示しています。

* **異なるキャッシュ サイズ**
  * コアによってキャッシュ サイズが異なる場合があります。
  * たとえば、高性能なコアの方が L1 および L2 キャッシュが大きい場合があります。
* **キャッシュの共有**
  * コア間でのキャッシュの共有レベルはさまざまです。
  * たとえば、高性能なコアはそれぞれ専用の L2 キャッシュを持ち、効率の高いコアは 1 つの L2 キャッシュを共有する場合があります。
* **同時マルチスレッディング (SMT) のサポート**
  * SMT をサポートするコアもあれば、サポートしないコアもあります。
  * Intel は SMT を *Hyper-Threading* と呼んでいます。
* **コア内部のレイアウト**
  * コアの種類によって、内部リソースが異なる場合があります。
  * たとえば、高性能なコアには、より多くの浮動小数点パイプや ALU パイプ、またはより広いデータ レーンが含まれる場合があります。
* **最大周波数**
  * コアの種類によって最大周波数が異なる場合があります。
  * たとえば、高性能なコアは、効率の高いコアよりも高い周波数を実現できる場合があります。

これらの変更は、CPU 内のコア間でパフォーマンスと消費電力のバランスを取るように設計されています。周波数の高いコアは、消費電力の増加と引き換えに高いパフォーマンスを発揮します。複数のコアがすべて同じキャッシュを共有すると、ダイ サイズが小さくなり消費電力が削減されますが、その代わりにパフォーマンスが低下します。目的は、パフォーマンスと消費電力をより動的に選択できるようにすることです。ユーザーがフル パフォーマンスを必要としない場合、OS は消費電力を削減するために効率の高いコアに切り替えます。

Windows では、CPU 上のすべてのコアが同じ [命令セット アーキテクチャ](https://en.wikipedia.org/wiki/Instruction_set_architecture) (ISA) をサポートしている必要があることに注意してください。この要件は、CPU が特定の命令セットをサポートしている場合、CPU 内のすべてのコアがその命令セットをサポートしていることを意味します。たとえば、CPU が AVX2 をサポートしていると示している場合、すべてのコアが AVX2 をサポートしています。

高性能なコアと効率の高いコアの違いの詳細については、各 IHV の公式ドキュメントを参照してください。

## パフォーマンスの数値

効率レベルの異なるコアは、それぞれ異なるパフォーマンスを示すことが予想されます。一方のコアはパフォーマンスのために設計され、もう一方は省電力のために設計されています。問題は、コア間にどの程度の違いがあるかです。

示されているデータは、複数の IHV のデータを集約したものです。各 IHV は、トポロジに関して行った選択に基づいて、テストごとに異なるスケーリングを示します。正確な数値は異なりますが、パターンはすべての IHV で類似しています。また、CPU の世代ごとに異なるトレードオフが行われるため、相対的な数値も変わります。既存のすべての IHV と世代にわたる数値を作成することは、この記事の範囲外です。

### 合成テスト

ベクトル演算は、ゲーム タイトルがフレーム内で行う作業の大部分を占めます。このワークロードには、物理演算から AI、レンダリングまで、あらゆるものが含まれます。ゲームは、単一の 128 ビット SSE レジスタに収まる 3 つまたは 4 つの float を持つベクトル型を使用して、この作業を実行します。これらは、内積、行列乗算、平方根の計算、ベクトル変換など、幅広い演算にわたります。これらの演算のパフォーマンスの違いは、全体的なフレーム時間に大きな影響を与える可能性があります。

次のグラフは、7 つの一般的な数学演算を実行する相対的なコストを示しています。測定には、単一の 128 ビット SSE レジスタに格納された 4 つの 32 ビット浮動小数点値のベクトルを使用しています。行列は 4 つのベクトルで構成され、4 つの 128 ビット SSE レジスタにわたって合計 16 個の 32 ビット浮動小数点値を持ちます。これらの演算は、フレーム内で一般的に使用されるため選択されました。

* **内積**: ベクトルの内積を計算します。
* **ベクトルの変換**: ベクトルに行列を乗算して変換します。
* **ベクトルの加算**: 2 つのベクトルを加算します。
* **外積**: 2 つのベクトルの外積を計算します。
* **ベクトルの正弦**: ベクトルの各要素について正弦の近似値を計算します。
* **ベクトル FMA (融合積和演算)**: 2 つのベクトルを乗算し、3 つ目のベクトルを加算します。
* **逆平方根**: ベクトルの長さの逆平方根を計算します。

**図 1: 演算の実行にかかった時間。低いほど良好です。**

![ヘテロジニアス合成ベンチマークの数値](https://mintcdn.com/microsoft-4404708b/5IpvKlT-jmAaVkeY/images/hybrid-cpu/heterogeneous-performance-delta-real-world.png?fit=max\&auto=format\&n=5IpvKlT-jmAaVkeY\&q=85\&s=e34c8ae53aa69a08d7db857ddb780060)

このグラフでは、y 軸は演算の実行にかかった時間です。各バーは、すべての IHV で測定された演算の時間の範囲を表します。高性能なコアまたは効率の高いコアのいずれかでテストを実行したときの最小時間と最大時間です。

予想どおり、コアの種類によってパフォーマンスは異なります。しかし、これらの数値から得られる重要なポイントは、パフォーマンスがすべての演算と IHV で同じようにスケーリングするわけではないということです。IHV 間の結果のばらつきは同程度である傾向がありましたが、ばらつきが大きいものもありました。ばらつきの大きい演算は、主にベクトルの正弦と逆平方根の演算で見られました。この違いは、高性能なコアと効率の高いコアのコア アーキテクチャの選択に起因します。この差の正確な原因は、複数の要因から生じる可能性があります。たとえば、高性能なコアの方が浮動小数点パイプが多く、より多くの演算を並列で実行できる場合があります。正確な理由は、IHV と CPU の世代ごとに固有です。特定の演算についてはパフォーマンスの差が存在しない構成もあり得ます。結局のところ、タイトルは IHV 間のこのばらつきを考慮する必要があります。

## 実際の環境

前述の数値は、特定のベンチマークを個別に示したものであり、全体像の一部しか示していません。次の数値は、2 つのメイン スレッド (Simulation と Render) と一連のジョブ スレッドを使用する AAA ゲーム エンジンのベンチマークから得られたものです。このベンチマークでは、3 つのテスト構成を使用します。

* 2 つのメイン スレッドは、高性能なコアでのみ実行されます。
* 2 つのメイン スレッドは、効率の高いコアでのみ実行されます。
* 2 つのメイン スレッドは、システム内のすべてのコアで実行されます。

各テスト構成では、ジョブ スレッドはシステム内のすべてのコアを自由に移動できます。レンダリング作業には、可能な限り最低の解像度と品質を使用します。この設定により、テストの実行全体を通じてタイトルが CPU バウンドになることが保証されます。また、この設定により、Simulation スレッドがフレーム レートを左右する要因になります。

このグラフは、フレーム中に各スレッドで費やされた時間を示しています。平均フレーム時間と 1% high の両方を示しています。1% high は、最も長い 1% のフレームのしきい値です。すべてのフレームのうち、この値を超えるのは 1% だけです。この数値により、スタッターが発生しているフレームの数をよく把握できます。平均から離れるほど、タイトルのスタッターはひどくなります。いずれの場合も、値が低いほど長いフレームが少なく、目に見えるスタッターが少ないことを示します。

前述のとおり、示されているデータは複数の IHV のデータを集約したものです。各 IHV は、トポロジに関して行った選択に基づいて、テストごとに異なるスケーリングを示します。正確な数値は異なりますが、パターンは IHV 間で類似しています。CPU の世代ごとに異なるトレードオフが行われる場合があり、それによって相対的な数値が変わります。既存のすべての CPU IHV と世代にわたる数値を作成することは、このドキュメントの範囲外です。

**図 2: 平均所要時間と 1% high。低いほど良好です。**

![ヘテロジニアス実環境ベンチマークの数値](https://mintcdn.com/microsoft-4404708b/5IpvKlT-jmAaVkeY/images/hybrid-cpu/heterogeneous-performance-delta-synthetic.png?fit=max\&auto=format\&n=5IpvKlT-jmAaVkeY\&q=85\&s=76639e4ca89ebd2e84a2f63a694204f2)

このグラフでは、y 軸は 1 フレームにかかった時間です。各バーは、すべての IHV で測定された演算の時間の範囲を表します。テストした各スレッド トポロジの最小時間と最大時間です。

OS のスレッド スケジューラは、ヒューリスティックを使用して、スレッドが実行する作業量を判断します。これらのヒューリスティックは、OS の選択と IHV によるドライバーの調整を組み合わせたものです。スケジューラはこの情報を使用して、特定のスレッドをどのコアで実行するかを選択します。スレッドが大量の作業を行っていると判断すると、スケジューラはそのスレッドをより高性能なコアに移動します。

スケジューラのヒューリスティックにより、重要なスレッドを自由に移動できるようにした方がタイトルの実行が速くなるという、直感に反する動作が発生することがあります。この動作は常に発生するわけではなく、ワークロード、コア トポロジ、および正確なスケジューラのヒューリスティックによって異なります。

ここでテストしたケースでは、Simulation スレッドは Render スレッドよりも各フレームの実行に多くの時間を費やしています。スケジューラは、このスレッドをより高性能なコアに配置することを優先します。また、Render スレッドは作業を待機して中断している時間が長いため、性能の低いコアに配置することを決定します。最終的な効果として、この決定により、ジョブ スレッドの 1 つに高性能なコアが空くことになります。Render スレッドがより多くの作業を実行していた場合、この状況は発生しない可能性があります。

しかし、このグラフから得られる最も興味深いポイントは、1% high の数値です。重要なスレッドが自由に移動できる場合に平均が同じであっても、1% high は、重要なスレッドが高性能なコアに固定されている場合よりも悪化します。これは、重要なスレッドが自由に移動できる場合、フレーム レートのスタッターが多くなることを意味します。最も一般的な理由は、スケジューラのヒューリスティックがスライディング ウィンドウで動作することです。フレームに必要な作業の急増に対応するには時間がかかります。その間、重要なスレッドが性能の低いコアで実行されている可能性があります。

> [!NOTE]
>
> 各メーカーは、これらのスケジューラのヒューリスティックを調整して、自社のハードウェアに合わせることができます。たとえば、AMD には Workload Profile Scheduling があり、Intel には Thread Director テクノロジがあります。また、各 IHV は、SoC の特定のトポロジに合わせてヒューリスティックを調整する傾向があります。

## 推奨事項

### スレッド アフィニティ

効率クラスが 1 つだけのデスクトップ マシンで実行する場合のスレッド アフィニティに関するアドバイスは、厳密なスレッド アフィニティを設定せず、スレッドがすべてのコアを自由に移動できるようにすることです。この不確実性の主な理由は、ユーザーのマシンで他にどのようなプロセスが実行されているかを事前に知ることができないためです。厳密なアフィニティを持つスレッドが、システム内の別の高優先度スレッドとコアを共有することになる可能性があります。最終的な効果として、タイトルのスレッドは実行時間が不足します。この競合がフレームにとって重要なスレッドで発生すると、タイトルのフレーム時間が不安定になる可能性があります。OS がタイトルのスレッドを移動して実行を継続できる場所を確保しておく必要があります。

ただし、ヘテロジニアス CPU で実行する場合は、このアドバイスを変更する必要があります。主な理由は、OS がタイトルのどのスレッドやジョブがフレームにとって重要であるかを必ずしも把握していないためです。OS にはフレームにとって重要と思われるスレッドを判断するためのヒューリスティックがありますが、それでも誤った選択をする可能性があります。それらのスレッドやジョブのいずれかを性能の低いコアに移動することを決定し、その結果、タイトルでパフォーマンスの問題が発生する可能性が高くなる場合があります。

推奨事項は、タイトルのスレッドが最低でも 3 ～ 4 個のコアで実行できるようにすることを目標とし、それ以上が望ましいということです。たとえば、高性能なコアが 1 つと性能の低いコアが 3 つあるローエンド システムでは、スレッドがすべてのコアを自由に移動できるようにする必要があります。高性能なコアが 3 つ以上あるマシンでは、フレームにとって重要なスレッドをそれらの高性能なコアのみに制限できます。

SMT もサポートする多数のコアを備えたハイエンド マシンでは、一部のタイトルがスレッドを物理コアに固定しようとする場合があります。この手法により、複数の高優先度スレッドが物理コアの同じリソースを共有する際の、物理コア レベルでの競合を回避できます。ただし、一般的には、このケースを検討することはお勧めしません。OS はすでにこの動作をシステム全体に適用しています。OS は最初に物理コア間でスレッドをスケジュールし、必要な場合にのみ追加の論理コアを使用します。

タイトルが検討すべき唯一のケースは、タイトルの重要なスレッドをサポートするのに十分な数の高性能なコアがシステムにある場合です。この場合、フレームにとって重要なスレッドを高性能なコアに固定することが望ましい場合があります。ただし、それらのスレッドは、すべての高性能なコアを自由に移動できるようにする必要があります。場合によっては、すべてのスレッドがすべてのコアを自由に移動できるようにするとパフォーマンスが向上することがありますが、この動作はすべての CPU メーカーで一貫しているわけではありません。

タイトルが、すべてのスレッドにわたってワークロードを完全に分散できるワーク スティーリング システムを実装している場合、どのジョブ スレッドもすべてのコアを自由に移動できるようにする必要があります。高優先度のジョブ スレッドか低優先度のジョブ スレッドかは関係ありません。ワーク スティーリング アルゴリズムが自動的に負荷を分散し、最短時間で最も多くの作業を完了できるようにします。高性能なコア上のスレッドは、フレーム中により多くのジョブを実行します。

フレームにとって重要なスレッドが複数ある場合は、フレームにとって重要な 2 つのスレッドが同じコアで実行されないようにアフィニティを設定すると便利な場合があります。たとえば、フレームにとって重要なスレッドが 2 つある場合、それらのアフィニティを互いの排他的論理和に設定すると、OS によってスレッドが互いに競合する可能性がなくなります。ただし、これも、フレームにとって重要な各スレッドが最低でも 3 ～ 4 個のコアを自由に移動できるようにしたうえで、このケースをサポートするのに十分なコアがある場合にのみ、このオプションを検討してください。

### スレッド トポロジ

ヘテロジニアス環境でスレッドに作業を割り当てる方法を検討する際、主なデータとなるのは、さまざまなタスクの長さと、それらが互いにどのように相互作用するかです。各タスクの平均的な長さはどのくらいか、タスク間の依存関係は何か、各タスクの優先度は何か、などです。

重要なスレッドやジョブで発生する時間の急増を、ジョブ スレッドはどの程度吸収できるでしょうか。それらのスレッドやジョブのいずれかの実行が 35% 遅くなった場合、フレーム時間にどの程度影響するでしょうか。たとえば、タスクの実行が長引いた場合、この遅延はフレームの残りの部分にどの程度影響するのか、そのタスクが完了するまでシステムは停止するのか、などです。

重要なスレッドやジョブに追加のオーバーヘッドを加えることで、ヘテロジニアス システムがフレーム時間にどの程度影響するかを判断すると便利な場合があります。テストとして、ジョブまたは作業ブロックの所要時間をランダムに 25% ～ 35% 増やしてみます。全体的なフレーム時間の変化の度合いは、ジョブ システムが実行の急増をどの程度吸収できるかを示します。理想的な状況は、フレーム時間がほとんど変化しないことです。これは、システムがフレーム時間の任意の急増を吸収できることを意味します。急増は、性能の低いコアでの実行、またはシステム内の別のランダムなプロセスによる CPU 時間の使用から発生します。

### スレッド間の通信

ロック プリミティブを使用したスレッド間の通信については、ある程度の検討が必要です。コンテキスト スイッチのオーバーヘッドは、新しいスレッドの実行が開始される前の受信側コアの [C ステート](https://en.wikipedia.org/wiki/ACPI) と密接に結びついています。新しいスレッドの実行を開始する前に、コアは実行状態である C0 になっている必要があります。最もコストが低いのは、コアがすでに実行中で、すでに C0 にある場合です。しかし、コアがアイドル状態の場合は、より低い C ステートにあります。C ステートのレベルは、より深いスリープ レベルに対応します。スリープが深いほど、コアの消費電力は少なくなります。しかし、より深いスリープから復帰して再び実行を開始するには、より多くのコストがかかります。最も一般的な低電力 C ステートは C1 で、2 ～ 7 μs のオーバーヘッドが加わる可能性があります。次の C ステートである C2 では、約 40 ～ 100 μs のオーバーヘッドが加わる可能性があります。正確な数値は IHV と SoC によって異なります。アイドル時にコアがどの C ステートになるかを制御する変数は多数あり、応答性と消費電力のバランスで決まります。低い C ステートは、高い C ステートよりも消費電力が少なくなります。コアが最初に C1 に入り、しばらくコードを実行していない場合に C2 に切り替わるのが一般的です。

もう 1 つの懸念事項は、ジョブを送信するスレッドとジョブを実行するスレッドが、パフォーマンス プロファイルの異なるコア上にある場合に何が起こるかです。技術的には、ジョブ スレッドが新しいジョブの作成よりも速くジョブを完了することがあり得ます。この状況は、ジョブを実行するスレッドが、ジョブを送信するスレッドよりも高速なコア上にある場合に発生しやすくなります。

ジョブの実行にかかる時間が作成にかかる時間よりも短い場合、タイトルは、ジョブ スレッドが新しいジョブを待機して常に中断するという状況に陥る可能性があります。最終的な効果として、ジョブ スレッドが常に中断するため、ジョブの実行コストが高くなります。各ジョブの実行時間に、コンテキスト スイッチのオーバーヘッド コストが加算されます。このオーバーヘッドにより、逆説的に、高性能なコアで実行するとフレーム レートが低下する可能性があります。たとえば、以前は 250 μs かかっていた作業が突然 275 μs かかるようになることがあり、この低下はパフォーマンスの 10% 低下に相当します。

このケースがあるため、ジョブは作成されるたびに送信するのではなく、バッチで送信することをお勧めします。たとえば、システムがジョブを構築し、ジョブを送信し、ジョブを構築し、ジョブを送信する、というパターンに従っている場合です。このパターンを調整して、まず一連の構築を実行し、その後すべてを同時に送信するようにする必要があります。

タイトルが [WaitForSingleObject](https://learn.microsoft.com/windows/win32/api/synchapi/nf-synchapi-waitforsingleobject) を必要とするオブジェクトを使用している場合は、小さなスピン ループ内に配置する必要があります。タイムアウト 0 で関数を呼び出し、スピンが完了したら、タイトルの通常のタイムアウト値を使用します。ただし、この問題に対する最善の解決策は、[Slim Reader/Writer (SRW) ロック](https://learn.microsoft.com/windows/win32/sync/slim-reader-writer--srw--locks)、[クリティカル セクション](https://learn.microsoft.com/windows/win32/sync/critical-section-objects)、[WaitOnAddress](https://learn.microsoft.com/windows/win32/api/synchapi/nf-synchapi-waitonaddress) などの最新の同期 API を使用することです。これらの同期 API はすべて、内部に何らかの形式のスピンを含んでおり、スレッドが常に中断するのを防ぐことができます。このスピンは、短時間保持されるロックで特に役立ちます。

長いスピン ロックは避けることが重要です。たとえば、スレッドが実行する作業を待機して常にスピンするパターンです。コア上で実行する必要のある他の作業がある場合、スケジューラは最終的にスピンしているスレッドを中断してこの作業を実行します。このプリエンプションが発生すると、中断されたスレッドがかなりの時間中断されたままになる可能性が高くなります。この動作はすべて、他のスレッドのリソース不足を防ぐために行われます。タイトルは中断のタイミングをほとんど制御できないため、フレーム レートのスタッターにつながる可能性があります。

### ジョブ システム

最高のパフォーマンスを得るには、専用の長時間実行されるフレームにとって重要なスレッドに依存するのではなく、ジョブを中心にエンジンを構成します。たとえば、Simulation/Render スレッド モデルでは、フレームにとって重要な永続的なスレッドが 2 つ作成されます。しかし、長いジョブがあると、どのジョブ スレッドもフレームにとって重要なスレッドとして動作する可能性があります。ジョブに 10 ms かかる場合、元の役割に関係なく、そのジョブを実行するスレッドはその期間中フレームにとって重要になります。

### ワーク スティーリング

最も重要な推奨事項は、ジョブ システム全体でワーク スティーリングを使用し、フレームにとって重要なスレッドをワーク スティーリング アルゴリズムに参加させることです。たとえば、レンダー スレッドがレンダリング関連のジョブの完了を待機している場合は、それらのジョブの一部を実行する必要があります。

この動作は、多くのゲーム エンジンですでに一般的な手法です。しかし、この種の動作は、ヘテロジニアス コアや OS によるプリエンプションの可能性に起因するフレームの急増をゲーム エンジンが吸収できるようにするために重要であるため、ここで言及しておく価値があります。

ジョブの実行に時間がかかると、フレーム時間の急増のリスクが高まります。この動作は、ジョブが性能の低いコアで実行されていること、または PC 内の他の作業によって発生する可能性があります。推奨事項は、エンジンが 1 つのジョブにかかる時間をフレームの約 2% にすることを目指すことです。この予算は、たとえば 60 fps で実行されるゲームでは約 333 μs 以下であることを意味します。必要に応じて、2% という数値を目指して、複数の小さなジョブを大きなジョブにバッチ処理する必要があります。このアプローチにより、ジョブのサイズ、スレッド間の通信、急増を吸収する能力の間で適切なトレードオフが得られます。

次の例は、フレームにとって重要な長いジョブがシステム パフォーマンスに与える影響を示しています。これらの例では、各ジョブは同じ数の演算で構成されています。この例では、1 つのジョブが低速なコアで 375 μs、高速なコアで 250 μs かかります。ロング ポール ジョブは、他のジョブよりも 6 倍コストが高くなります。

最悪のケースは、ロング ポール ジョブが低速なコアのいずれかで実行される場合です。これは、フレーム時間に大きな影響を与える可能性があります。このシナリオでは、作業の実行にかかる合計時間は 375 μs の 6 倍である 2,250 μs になります。

**図 3: 低速なコアで実行されるロング ポールの例。**

![低速なコアでのヘテロジニアス ワーク スティーリングのロング ポールの例](https://mintcdn.com/microsoft-4404708b/5IpvKlT-jmAaVkeY/images/hybrid-cpu/heterogeneous-work-stealing-part-one.png?fit=max\&auto=format\&n=5IpvKlT-jmAaVkeY\&q=85\&s=1c969ca714faf873659c1bb93d217bb2)

ロング ポール ジョブが高速なコアで実行される場合は、状況が改善されます。この場合、合計時間は 1,500 μs です。まだ十分ではありませんが、改善されています。

**図 4: 高速なコアで実行されるロング ポールの例。**

![高速なコアでのヘテロジニアス ワーク スティーリングのロング ポールの例](https://mintcdn.com/microsoft-4404708b/5IpvKlT-jmAaVkeY/images/hybrid-cpu/heterogeneous-work-stealing-part-two.png?fit=max\&auto=format\&n=5IpvKlT-jmAaVkeY\&q=85\&s=4ea3d614e652ddc0b1aa7220916ecd6c)

最良の結果は、ジョブのサイズを小さく均等に分散させて、ロング ポール ジョブを回避することで得られます。この例では、合計実行時間は 1,125 μs に短縮され、同じ量の作業を完了しながら、前の例の半分の時間になります。

**図 5: 適切なジョブの長さを使用してロング ポール ジョブを回避します。**

![適切なジョブの長さを使用したヘテロジニアス ワーク スティーリング](https://mintcdn.com/microsoft-4404708b/5IpvKlT-jmAaVkeY/images/hybrid-cpu/heterogeneous-work-stealing-part-three.png?fit=max\&auto=format\&n=5IpvKlT-jmAaVkeY\&q=85\&s=fc6d6450688e351f5e2595236f1e9ffb)

さらに重要なのは、OS がジョブの実行中に割り込もうとした場合でも、システムが自動的にワークロードを再分散することです。この場合、OS は高速なコアで 375 μs のタスクによってタイトルに割り込みました。この再分散は全体的なフレーム時間に影響を与えず、1,125 μs のままです。ジョブの 1 つが、低速なコアの空き時間に移動しました。

**図 6: ワークロードの再分散を伴う OS の割り込みでも、安定したフレーム時間が維持されます。**

![割り込みを伴う、適切なジョブの長さを使用したヘテロジニアス ワーク スティーリング](https://mintcdn.com/microsoft-4404708b/5IpvKlT-jmAaVkeY/images/hybrid-cpu/heterogeneous-work-stealing-part-four.png?fit=max\&auto=format\&n=5IpvKlT-jmAaVkeY\&q=85\&s=de188bd300c0985fd0028064ca234d9d)

これらの例は、ロング ポール ジョブをすぐに開始できる場合のみを対象としており、ジョブ キューの後半まで開始できない場合は状況がさらに悪化します。すべては [アムダールの法則](https://en.wikipedia.org/wiki/Amdahl%27s_law) に帰着します。この法則では、システムの実行時間は、そのシステムで最も長く実行される部分に結びついています。エンジンは、それらのロング ポール スレッドの実行時間を増加させる急増を吸収することはできません。これらの急増は、低速なコアでの実行や、システム内の他の高優先度プロセスによる割り込みから発生する可能性があります。

## まとめ

これらの推奨事項を実装することで、ゲームがさまざまな CPU アーキテクチャで確実に動作し、他の Windows プロセスからの干渉に対しても回復力を維持できるようになります。

## 付録

### コアの効率クラスの判断

[GetLogicalProcessorInformationEx](https://learn.microsoft.com/windows/win32/api/sysinfoapi/nf-sysinfoapi-getlogicalprocessorinformationex) 関数を使用して、システム内の各プロセッサの効率クラスを判断します。関数から返されたデータを反復処理し、効率クラスとコア マスクを含む [RelationProcessorCore](https://learn.microsoft.com/windows/win32/api/winnt/ns-winnt-processor_relationship) ブロックを探します。

```cpp theme={null}
char* processorInfoData = nullptr;
DWORD processorInfoDataSize = 0;

GetLogicalProcessorInformationEx(RelationAll, nullptr, &processorInfoDataSize);
processorInfoData = new char[processorInfoDataSize];
GetLogicalProcessorInformationEx(RelationAll, processorInfoData, &processorInfoDataSize);

auto workingDataBuffer = processorInfoData;
auto dataLeft = processorInfoDataSize;
while (dataLeft)
{
    auto procInfo = (SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX*) workingDataBuffer;
    switch (procInfo->Relationship)
    {
    case RelationProcessorCore:
        {
            processorEfficiency[procInfo->Processor.GroupMask[0].Mask] = 
                  procInfo->Processor.EfficiencyClass;
        }
        break;
    }
    dataLeft -= procInfo->Size;
    workingDataBuffer += procInfo->Size;
}
```

### 電力使用量に基づくスレッドの固定

前述のとおり、マシン上の他のプロセスからの干渉の可能性があるため、デスクトップ環境ではアフィニティを使用したコアの固定はお勧めしません。ただし、Windows には、タイトルが作業を実行する場所を推奨するヒントを OS に提供する方法が用意されています。このガイダンスを行う方法の 1 つは、電源管理 API を使用することです。

```cpp theme={null}
    THREAD_POWER_THROTTLING_STATE PowerThrottling;
    ZeroMemory(&PowerThrottling, sizeof(PowerThrottling));
    PowerThrottling.Version = THREAD_POWER_THROTTLING_CURRENT_VERSION;

    // 
    // EcoQoS 
    // Turn EXECUTION_SPEED throttling on. 
    // ControlMask selects the mechanism 
    // and StateMask declares which mechanism should be on or off. 
    // 

    PowerThrottling.ControlMask = THREAD_POWER_THROTTLING_EXECUTION_SPEED;
    PowerThrottling.StateMask = THREAD_POWER_THROTTLING_EXECUTION_SPEED;

    SetThreadInformation(GetCurrentThread(),
        ThreadPowerThrottling,
        &PowerThrottling,
        sizeof(PowerThrottling));

    // 
    // HighQoS 
    // Turn EXECUTION_SPEED throttling off. 
    // ControlMask selects the mechanism
    // and StateMask is set to zero as mechanisms should be turned off. 
    // 

    PowerThrottling.ControlMask = THREAD_POWER_THROTTLING_EXECUTION_SPEED;
    PowerThrottling.StateMask = 0;

    SetThreadInformation(GetCurrentThread(),
        ThreadPowerThrottling,
        &PowerThrottling,
        sizeof(PowerThrottling));

    // 
    // Let system manage all power throttling. ControlMask is set to 0 as we don't want 
    // to control any mechanisms. 
    // 

    PowerThrottling.ControlMask = 0;
    PowerThrottling.StateMask = 0;

    SetThreadInformation(GetCurrentThread(),
        ThreadPowerThrottling,
        &PowerThrottling,
        sizeof(PowerThrottling));
```

### CPU セットを使用した効率クラスのクエリ

[CPU セット](https://learn.microsoft.com/windows/win32/procthread/cpu-sets) を使用することも、システム内の各コアの効率クラスをクエリするもう 1 つの方法です。

```cpp theme={null}
bool QueryCPUSets()
{
    ULONG requiredSize = 0;
    (void)GetSystemCpuSetInformation(nullptr, 0, &requiredSize, GetCurrentProcess(), 0);
    if (requiredSize == 0)
        return false;

    std::vector<uint8_t> rawBuffer(requiredSize);
    PSYSTEM_CPU_SET_INFORMATION cpuSets =
               reinterpret_cast<PSYSTEM_CPU_SET_INFORMATION>(rawBuffer.data());

    if (!GetSystemCpuSetInformation(cpuSets, requiredSize, 
               &requiredSize, GetCurrentProcess(), 0))
        return false;

    std::vector<CoreEfficiencyInfo> cores;
    std::map<uint8_t, uint32_t> efficiencyLevelCounts;

    uint8_t* current = rawBuffer.data();
    uint8_t* end = current + requiredSize;

    while (current < end)
    {
        PSYSTEM_CPU_SET_INFORMATION info =    
                 reinterpret_cast<PSYSTEM_CPU_SET_INFORMATION>(current);

        if (info->Type == CpuSetInformation)
        {
            CoreEfficiencyInfo coreInfo = {};
            coreInfo.cpuSetId = info->CpuSet.Id;
            coreInfo.coreIndex = info->CpuSet.CoreIndex;
            coreInfo.logicalProcessorIndex = info->CpuSet.LogicalProcessorIndex;
            coreInfo.efficiencyClass = info->CpuSet.EfficiencyClass;
            coreInfo.group = info->CpuSet.Group;
            cores.push_back(coreInfo);

            efficiencyLevelCounts[coreInfo.efficiencyClass]++;
        }

        current += info->Size;
    }
    return true;
}
```

### CPU セットを使用したスレッドの効率クラスへの割り当て

[CPU セット](https://learn.microsoft.com/windows/win32/procthread/cpu-sets) は、スレッドのアフィニティ マスクを明示的に設定する代わりに使用することもできます。明示的なアフィニティ マスクではなく、CPU セットを使用することをお勧めします。

```cpp theme={null}
bool LockThreadToEfficiencyClass1(HANDLE thread, 
                   const std::vector<CoreEfficiencyInfo>& cores,uint32_t efficiencyClass)
    {
        std::vector<ULONG> cpuSetIds;
        for (const auto& core : cores)
        {
            if (core.efficiencyClass == efficiencyClass)
            {
                cpuSetIds.push_back(core.cpuSetId);
            }
        }

        if (cpuSetIds.empty())
            return false;

        if (!SetThreadSelectedCpuSets(thread, cpuSetIds.data(),
                        static_cast<ULONG>(cpuSetIds.size())))
            return false;

        return true;
}
```


## Related topics

- [パフォーマンスと電力効率のバランス](https://docs.xbox.com/ja-jp/build/gdk-and-engines/handheld/handheld-performance.md)
- [ソーシャル マネージャーのメモリとパフォーマンス](https://docs.xbox.com/ja-jp/services/xbox-services/community/social-manager/concepts/live-socmgr-mem-perf.md)
- [Visual Studio でのプロファイルガイド付き最適化](https://docs.xbox.com/ja-jp/tools/tools-console/pgo/pgo_full.md)
- [PC ゲーム開発環境とツール](https://docs.xbox.com/ja-jp/tools/tools-pc/gc-tools-pc-toc.md)
- [パフォーマンスとリテンション](https://docs.xbox.com/ja-jp/services/playfab/data-analytics/legacy/insights/performance-retention.md)
