ハードウェア
各独立系ハードウェア ベンダー (IHV) は、ヘテロジニアス CPU トポロジを異なる形で定義しています。Intel と Qualcomm は Performance と Efficiency という用語を使用し、AMD は Performance と Compact を使用します。各ベンダーは、コア トポロジを設計する際にパフォーマンスと効率の間でトレードオフを行います。 以下の例は、関わってくる設計選択の一部を示しています。- 異なるキャッシュ サイズ
- 異なるコアで異なるキャッシュ サイズを持つ場合があります。
- たとえば、高性能コアはより大きな L1 と L2 キャッシュを持つ場合があります。
- キャッシュの共有
- コア間のキャッシュ共有には多様なレベルがあり得ます。
- たとえば、高性能コアは各コアがプライベート L2 キャッシュを持つ一方、高効率コアは単一の L2 キャッシュを共有する場合があります。
- 対称マルチ スレッディング (SMT) のサポート
- 一部のコアは SMT をサポートし、他のコアはサポートしない場合があります。
- Intel は SMT を Hyper-Threading と呼びます。
- 内部コア レイアウト
- 各種コアの内部リソースは異なる場合があります。
- たとえば、高性能コアはより多くの浮動小数点パイプ、ALU パイプ、または広いデータ レーンを含む場合があります。
- 最大周波数
- コア間で最大周波数が異なる場合があります。
- たとえば、高性能コアは高効率コアよりも高い周波数を達成できる場合があります。
パフォーマンスの数値
異なる効率レベルのコアは異なるパフォーマンスを持つと予想されます。1 つはパフォーマンス向けに、もう 1 つは節電向けに設計されています。問題は、コア間でどれほど異なるかということです。 示すデータは IHV 横断の集合体です。各 IHV は自社トポロジの選択に基づき、テストごとに異なるスケーリングを持ちます。厳密な数値は異なりますが、パターンはすべての IHV にわたって類似しています。また、CPU 世代ごとに異なるトレードオフを行うため、相対的な数値が調整されます。既存のすべての IHV と世代にわたる数値の作成は、この記事の範囲を超えています。合成テスト
ベクトル演算は、ゲーム タイトルがフレーム内で行う作業の大部分を占めます。このワークロードは、物理演算、AI、レンダリングまで多岐にわたる可能性があります。ゲームはこの作業を、単一の 128 ビット SSE レジスタに収まる 3 つまたは 4 つの float を持つベクトル型を使って実行します。ドット積、行列乗算、平方根計算、ベクトル変換など幅広い演算が対象となります。これらの演算のパフォーマンスの違いは、全体のフレーム時間に大きな影響を与える可能性があります。 以下のグラフは、7 つの一般的な数学演算を実行するときの相対的コストを示しています。測定には、単一の 128 ビット SSE レジスタに格納された 4 つの 32 ビット浮動小数点値のベクトルを使用します。行列は 4 つのベクトルで構成され、合計 16 個の 32 ビット浮動小数点値が 4 つの 128 ビット SSE レジスタに配置されます。これらの演算は、フレーム内で一般的に使用されるため選ばれました。- ドット積: ベクトルのドット積を計算します。
- ベクトル変換: ベクトルを行列で乗算して変換します。
- ベクトル加算: 2 つのベクトルを加算します。
- クロス積: 2 つのベクトルのクロス積を計算します。
- ベクトル sine: ベクトルの各要素の sine の近似を計算します。
- ベクトル FMA (fused multiply-add): 2 つのベクトルを乗算し、3 つ目を加算します。
- 逆平方根: ベクトルの長さの逆平方根を計算します。

実世界
前述の数値は、単独の特定のベンチマークを示しており、話の一部にすぎません。以下の数値は、2 つのメイン スレッド(Simulation と Render)とジョブ スレッドのセットを使用する AAA ゲーム エンジンのベンチマークからのものです。ベンチマークでは 3 つのテスト構成を使用します。- 2 つのメイン スレッドは高性能コアでのみ実行されます。
- 2 つのメイン スレッドは高効率コアでのみ実行されます。
- 2 つのメイン スレッドはシステム内のすべてのコアで実行されます。

各メーカーは、自社ハードウェアに合わせてこれらのスケジューラー ヒューリスティックを調整できます。たとえば、AMD には Workload Profile Scheduling があり、Intel には Thread Director テクノロジがあります。各 IHV は SoC の特定のトポロジに合わせてヒューリスティックを調整する傾向があります。
推奨事項
スレッド アフィニティ
単一の効率クラスしか持たないデスクトップ マシンで実行する場合、スレッド アフィニティに関する助言は、ハード スレッド アフィニティを設定せず、代わりにスレッドがすべてのコアにわたって自由に動くようにすることです。この不確実性の主な理由は、ユーザーのマシンで他のプロセスがあらかじめ実行されている可能性があるためです。ハード アフィニティを持つスレッドが、システム内の別の高優先度スレッドとコアを共有することになる可能性があります。正味の効果として、タイトル スレッドは実行時間に飢えます。この競合がフレーム クリティカルなスレッドで発生すると、タイトルは一貫しないフレーム時間を経験する可能性があります。OS がタイトル スレッドを移動させて実行し続ける場所があることを確認したいのです。 しかし、この助言はヘテロジニアス CPU で実行する場合には変わる必要があります。主な理由は、OS はタイトルのどのスレッドやジョブがフレーム クリティカルかを必ずしも知らないためです。フレーム クリティカル スレッドと思われるものを判定するヒューリスティックはありますが、そのヒューリスティックは依然として誤った選択を行う可能性があります。それらのスレッドやジョブの 1 つを性能の低いコアに移動させ、その結果、タイトルにおけるパフォーマンス問題の可能性を高める場合があります。 推奨事項は、タイトル スレッドが最小で 3〜4 コアで実行できるようにすることを目指し、さらに多い方が望ましいということです。たとえば、1 つの高性能コアと 3 つの低性能コアを持つローエンド システムでは、スレッドをすべてのコアにわたって自由に動かすべきです。3 つ以上の高性能コアを持つマシンでは、フレーム クリティカルなスレッドをそれら高性能コアのみに制限できます。 多くのコアを持ち SMT もサポートするハイエンド マシンでは、一部のタイトルはスレッドを物理コアにロックしようとするかもしれません。この手法は、複数の高優先度スレッドが物理コアの同じリソースを共有する物理コア レベルでの競合を回避します。ただし、一般的には、このケースを検討することは推奨しません。OS はすでにこの挙動をシステム全体に適用しています。まず物理コアにわたってスレッドをスケジュールし、必要な場合にのみ論理コアをさらに使用します。 タイトルが検討すべき唯一のケースは、タイトルのクリティカル スレッドをサポートするのに十分な高性能コアがシステム内にある場合です。この場合、フレーム クリティカル スレッドを高性能コアにロックすることが望ましいかもしれません。ただし、それらのスレッドはすべての高性能コアにわたって自由に動けるようにする必要があります。すべてのスレッドをすべてのコアにわたって自由に動かすとパフォーマンスが向上する場合がありますが、この挙動は CPU メーカー間で一貫していません。 タイトルがすべてのスレッドにわたってワークロードを完全にバランスできるワーク スティーリング システムを実装している場合、任意のジョブ スレッドはすべてのコアにわたって自由に動くようにすべきです。高優先度でも低優先度でも構いません。ワーク スティーリング アルゴリズムが自動的に負荷をバランスし、最も多くの作業を最短時間で完了できるようにします。より高性能なコア上のスレッドは、フレーム中により多くのジョブを実行します。 複数のフレーム クリティカル スレッドがある場合、それらのアフィニティを、2 つのフレーム クリティカル スレッドが同じコアで実行されないように設定すると便利な場合があります。たとえば、2 つのフレーム クリティカル スレッドがある場合、それらのアフィニティを互いの排他的論理和 (XOR) に設定すると、OS がスレッドを競合させる可能性を排除できます。ただし、繰り返しますが、このオプションは、各フレーム クリティカル スレッドが最小で 3〜4 コアにわたって自由に動くのを許容しつつ、このケースをサポートするのに十分なコアがある場合にのみ検討してください。スレッド トポロジ
ヘテロジニアス環境でスレッドに作業を割り当てる方法を考える際、主なデータは各種タスクの長さと相互作用に関するものです。各タスクの平均長さは何か、タスク間の依存関係は何か、各タスクの優先順位は何か、などです。 ジョブ スレッドは、クリティカル スレッドやジョブの時間スパイクをどれだけ吸収できるでしょうか? それらのスレッドやジョブの 1 つが 35% 遅くなった場合、フレーム時間にどれほど影響するでしょうか? たとえば、あるタスクが長引いた場合、この遅延がフレームの残りにどれほど影響するか、システムはそのタスクが完了するまでストールするでしょうか? クリティカル スレッドやジョブに余分なオーバーヘッドを加えることで、ヘテロジニアス システムがフレーム時間にどれほど影響するかを判定することは有用です。テストとして、ランダムにジョブや作業ブロックが余分に 25%〜35% の時間を要するようにしてください。全体のフレーム時間の変化の度合いは、ジョブ システムが実行スパイクをどれだけ吸収できるかを示します。理想的な状況は、フレーム時間がほとんど変化しないことです。それは、システムが任意のフレーム時間スパイクを吸収できることを意味します。スパイクは、性能の低いコアで実行されることによるものか、CPU 時間を使うシステム内の別のランダム プロセスから来るものです。スレッド通信
ロック プリミティブを使用したスレッド通信については、多少の考察が必要です。コンテキスト スイッチのオーバーヘッドは、新しいスレッドが実行を開始する前の受信側コアの C-State と密接に結びついています。新しいスレッドが実行を開始する前に、コアは実行状態である C0 になっている必要があります。最も安価なのは、コアがすでに実行中で、すでに C0 にある場合です。ただし、コアがアイドル状態なら、より低い C-State にあります。C-State のレベルは、より深いスリープ レベルに対応します。より深いスリープは、コアの消費電力を減らします。ただし、より深いスリープから起きて実行を再開するのはより高コストです。最も一般的な低電力 C-state は C1 で、2〜7 μs のオーバーヘッドを追加できます。次の C-state である C2 は、約 40〜100 μs のオーバーヘッドを追加できます。正確な数値は IHV と SoC 固有です。アイドル時のコアがどの C-State にあるかを制御する変数は多くあり、応答性と消費電力のバランスです。より低い C-State はより高い C-State よりも消費電力が少ないです。コアはまずアイドル時に C1 に入り、しばらくコードを実行していなければ C2 に切り替わることが一般的です。 もう 1 つの懸念は、ジョブを送信するスレッドとジョブを実行するスレッドが異なるパフォーマンス プロファイルを持つコアにある場合に何が起こるかです。技術的には、ジョブ スレッドが新しいジョブが作成されるよりも速くジョブを完了できることが可能です。ジョブを実行するスレッドがジョブを送信するスレッドより高速なコアにある場合、この状況が発生する可能性が高くなります。 ジョブの実行時間が作成時間よりも短い場合、タイトルはジョブ スレッドが新しいジョブを待つために絶えずサスペンドされる状況に陥る可能性があります。正味の効果は、ジョブ スレッドが絶えずサスペンドされるため、ジョブの実行が高コストになることです。コンテキスト スイッチのオーバーヘッド コストが各ジョブの実行時間に加算されます。このオーバーヘッドは、逆説的により高性能なコアで実行するときにフレーム レートが低下する原因となり得ます。たとえば、以前は 250 μs かかっていた作業が突然 275 μs かかり始めることがあり、この後退はパフォーマンスの 10% 低下です。 このケースのため、ジョブは作成時に個別に送信するのではなく、バッチで送信することが推奨されます。たとえば、システムがジョブを作成、送信、作成、送信、というパターンに従う場合、代わりにビルドのバッチを最初に実行してから一度にすべて送信するようにパターンを調整すべきです。 タイトルが WaitForSingleObject を必要とするオブジェクトを使用している場合、小さなスピン ループ内に配置すべきです。タイムアウトを 0 として関数を呼び出し、スピンが終わったらタイトルの通常のタイムアウト値を使用してください。ただし、この問題の最良の解決策は、Slim Reader/Writer (SRW) Locks、Critical Sections、WaitOnAddress のような近代的な同期 API を使用することです。これらの同期 API はいずれも内部に何らかのスピンを含み、スレッドが絶えずサスペンドされるのを防ぎます。このスピンは、短時間だけ保持されるロックにとって特に有用です。 長いスピン ロックを避けることが重要です。たとえば、スレッドが実行する作業を絶えず待ち続けるパターンです。コアで他の作業を実行する必要がある場合、スケジューラーは最終的にスピンしているスレッドをサスペンドしてその作業を実行します。このプリエンプションが発生すると、サスペンドされたスレッドはかなりの時間サスペンドされたままである可能性が高くなります。この挙動は、他のスレッドの飢餓を防ぐために行われます。タイトルはサスペンドが発生するタイミングをほとんど制御できないため、フレーム レートのスタッターにつながる可能性があります。ジョブ システム
ベスト パフォーマンスのため、エンジンを専用の長時間実行フレーム クリティカル スレッドに依存させるのではなく、ジョブを中心に構造化してください。たとえば、Simulation/Render スレッド モデルは 2 つの永続的なフレーム クリティカル スレッドを作成します。ただし、長時間のジョブは、任意のジョブ スレッドがフレーム クリティカル スレッドとして振る舞う原因になり得ます。ジョブが 10 ms かかると、そのスレッドはその期間、元の役割に関係なくフレーム クリティカルになります。ワーク スティーリング
最重要の推奨事項は、ジョブ システム全体でワーク スティーリングを使用し、フレーム クリティカル スレッドもワーク スティーリング アルゴリズムに参加させることです。たとえば、レンダー スレッドがレンダー関連のジョブの完了を待っている場合、それらのジョブの一部を実行すべきです。 この挙動は、多くのゲーム エンジンですでに一般的な実践です。ただし、ヘテロジニアス コアと OS プリエンプションによるフレーム スパイクをゲーム エンジンが吸収できるためには、このような挙動が重要であるため、ここで言及する価値があります。 ジョブが実行に長時間かかると、フレーム時間スパイクのリスクが増加します。この挙動は、性能の低いコアでジョブが実行されるか、PC 内の他の作業によって発生することがあります。エンジンは、ジョブがフレームの約 2% を占めるように目指すことが推奨されます。この予算は、たとえば 60 fps で実行するゲームでは約 333 μs 以下ということです。必要に応じて、複数の小さなジョブをより大きなジョブにバッチ処理して、2% の数値を目指すべきです。このアプローチは、ジョブ サイズ、スレッド通信、スパイクの吸収能力の良いトレードオフを提供します。 これらの例は、長いフレーム クリティカル ジョブがシステム パフォーマンスに与える影響を示しています。これらの例では、各ジョブは同じ数の演算で構成されています。この例では、1 つのジョブが遅いコアで 375 μs、速いコアで 250 μs かかります。長い柱ジョブは他のジョブの 6 倍高コストです。 最悪のケースは、長い柱ジョブが遅いコアの 1 つに割り当てられた場合です。これはフレーム時間に劇的な影響を与えることがあります。このシナリオでは、作業を実行する総時間は 2,250 μs で、375 μs の 6 倍です。 図 3: 遅いコアで実行される長い柱の例。



