開発者にとって、スレッド化はゲームパフォーマンスに影響を与える重要な問題です。Apple Siliconゲームにおけるタスクスケジューリングの仕組みをご紹介します。
GPUとCPUへの要求は、現代のコンピューターにおいて最も計算負荷の高いワークロードの一つです。毎フレーム、数百、数千ものGPUジョブを処理する必要があります。
Apple Silicon上でゲームを可能な限り効率的に動作させるには、コードを最適化することが必要です。最大の効率性こそが、ここでの目標です。
Apple Siliconは、高速アクセスとパフォーマンス向上のために、新しい内蔵GPUとRAMを導入しました。Apple FabricはM1-M3アーキテクチャの一部であり、CPU、GPU、そして統合メモリへのアクセスを可能にします。メモリを他のストアにコピーする必要がなく、パフォーマンスが向上します。
コア
Apple Silicon CPUには、効率コアとパフォーマンスコアが搭載されています。効率コアは極めて低消費電力モードで動作するように設計されており、パフォーマンスコアはコードを可能な限り高速に実行するように作られています。
スレッド、つまりコード実行のパスは、ライブスレッドの場合、両方の種類のコア上でスケジューラによって自動的に実行されます。開発者は、スレッドの実行タイミングを制御し、スリープ状態にしたり、ウェイクアップさせたりすることができます。
実行時には、複数のソフトウェア レイヤーが 1 つ以上の CPU コアと対話して、プログラムの実行を調整できます。
これらには次のものが含まれます。
- XNUカーネルとスケジューラ
- Machマイクロカーネルコア
- 実行スケジューラ
- POSIXポータブルUNIXオペレーティングシステム層
- Grand Central Dispatch、または GCD (ブロックに基づく Apple 固有のスレッド技術)
- NSオブジェクト
- アプリケーション層
NSObjects は、Apple が 1997 年に Steve Jobs の 2 番目の会社 NeXT を買収した際に取得した NeXTStep オペレーティング システムによって定義されたコア コード オブジェクトです。
GCD ブロックは、コードのセクションを実行することによって機能し、完了するとコールバックまたはクロージャを使用して作業を終了し、何らかの結果を提供します。
POSIXには、コード実行への独立したパスであるpthreadが含まれています。AppleのNSThreadオブジェクトは、pthreadとその他のスケジューリング情報を含むマルチスレッドクラスです。NSThreadsとその関連クラスであるNSTaskを使用して、CPUコアで実行されるタスクをスケジュールできます。
これらすべてのレイヤーは連携して動作し、オペレーティング システムとアプリのソフトウェア実行を実現します。
ガイドライン
ゲームを開発する際には、最高のパフォーマンスを実現するために留意すべき点がいくつかあります。
まず、全体的な設計目標は、CPUコアとGPUにかかる負荷を軽減することです。最も高速に実行されるコードは、実行される必要のないコードです。
コードを減らし、実行スケジュールを最大化することは、ゲームをスムーズに実行し続けるために最も重要です。
Appleは、CPU効率を最大限に高めるための推奨事項をいくつか提供しています。これらのガイドラインは、IntelベースのMacにも適用されます。
アイドル時間とスケジュール
まず、特定のGPUコアが使用されていないときはアイドル状態になります。使用のために起動されると、わずかなウェイクアップ時間が発生しますが、これはわずかなコストです。Appleはこれを次のように示しています。
次に、2つ目のコスト、つまりスケジューリングがあります。コアが起動すると、OSスケジューラがどのコアでタスクを実行するかを決定するのに少し時間がかかり、その後、そのコアでコード実行をスケジュールして実行を開始する必要があります。
セマフォまたはスレッドシグナリングも設定して同期する必要があり、これには少し時間がかかります。
3 番目に、スケジューラがどのコアがすでにタスクを実行しており、どのコアが新しいタスクに使用できるかを判断するため、同期の遅延が発生します。
これらのセットアップコストはすべて、ゲームのパフォーマンスに影響を与えます。実行中に何百万回もの反復処理が行われると、これらの小さなコストが積み重なり、全体的なパフォーマンスに影響を与える可能性があります。
Apple Instrumentsアプリを使えば、これらのコストが実行時のパフォーマンスにどのような影響を与えるかを把握し、追跡することができます。Appleは、Instrumentsアプリでゲームを実行している例を以下のように示しています。
この例では、同じCPUコア上で開始/待機スレッドパターンが発生しています。これらのタスクは、パフォーマンスを向上させるために複数のコアで並列実行することもできたはずです。
この並列性の低下は、コード実行時間が非常に短いことが原因で発生します。この時間は、場合によってはシングルコアCPUのウェイクアップ時間とほぼ同じくらい短いこともあります。この短いコード実行をもう少し遅らせることができれば、別のコアで実行することができ、実行速度を向上させることができたはずです。
この問題を解決するために、Appleは適切なジョブスケジューリング粒度の使用を推奨しています。つまり、非常に小さなジョブを大きなジョブにグループ化し、全体の実行時間がコアのウェイクアップとスケジュールのオーバーヘッド時間に近づいたり超えたりしないようにすることです。
スレッドが実行されるたびに、常にわずかなスレッドスケジューリングコストが発生します。1つのスレッドで複数の小さなタスクを同時に実行すると、全体のスレッドスケジューリング数を削減できるため、スレッドスケジューリングに関連するスケジューラのオーバーヘッドの一部を削減できます。
次に、実行スケジュールを設定する前に、ほとんどのジョブを一度に実行準備しておきます。スレッドのスケジュール設定が開始されると、通常は一部のジョブが実行されますが、実行スケジュールの設定を待たなければならないジョブは、コアから移動されてしまう可能性があります。
スレッドがコアから移動されると、スレッドブロッキングが発生します。一般的に、スレッドのシグナルや待機はパフォーマンスの低下につながる可能性があります。
スレッドを繰り返し起動および一時停止すると、パフォーマンスの問題が発生する可能性があります。
ネストされた for ループを並列化する
ネストされたforループの実行中、外側のループのスケジュールを粗くする(つまり、実行頻度を低くする)ことで、ループの内側の部分は中断されずに済みます。これにより、全体的なパフォーマンスが向上します。
これにより、CPU キャッシュのレイテンシも短縮され、スレッドの同期ポイントも削減されます。
ジョブプールとカーネル
Apple は、パフォーマンスを向上させるためにワーカースレッドを活用するジョブプールの使用も推奨しています。
一般的に、ワーカー スレッドは、通常タスク スレッドと呼ばれる別のスレッドに代わって、または OS 自体に対してアプリの上位レベルの部分に代わって、何らかの作業を実行するバックグラウンド スレッドです。
ワーカースレッドはソフトウェアのさまざまな部分から生成されます。ワーカースレッドの中には、アクティブに実行されていない、あるいは実行がスケジュールされていない状態(スリープ状態)にできるものもあります。
ジョブプールでは、ワーカースレッドが他のスレッドからジョブスケジューリングを奪います。すべてのスレッドに一定のスレッドスケジューリングコストがかかるため、ジョブスティーリングにより、スケジューラが実行されるOSカーネル空間でジョブを開始するよりも、ユーザー空間でジョブを開始する方がはるかに低コストになります。
これにより、カーネル内のスケジューリングのオーバーヘッドが排除されます。
OSカーネルはOSの中核であり、バックグラウンド処理や低レベルの処理の大部分がここで行われます。ユーザー空間は、ワーカースレッドを含むほとんどのアプリやゲームのコード実行が実際に実行される場所です。
ユーザー空間でジョブスティーリングを使用すると、カーネルスケジューリングのオーバーヘッドが回避され、パフォーマンスが向上します。覚えておいてください。可能な限り最速のコードとは、実行される必要のないコードです。
合図や待ち時間を避ける
新しいジョブを作成する代わりに既存のジョブを再利用する場合(スレッドまたはタスクポインタを再利用する)、アクティブなコア上の既にアクティブなスレッドを使用することになります。これにより、ジョブスケジューリングのオーバーヘッドも削減されます。
また、ワーカースレッドは必要な場合にのみ起動するようにしてください。スレッドを起動して実行させるだけの十分な作業量があることを確認してください。
CPUサイクル
次に、実行時に無駄にならないように CPU サイクルを最適化します。
これを実現するには、まずEコアからPコアへのスレッドの昇格を避けます。Eコアは電力とバッテリー寿命を節約するために動作速度が遅くなります。
これを実現するには、CPUコアを独占するビジーウェイトサイクルを回避する必要があります。スケジューラがビジー状態のコアで長時間待機しなければならない場合、タスクを別のコア(利用可能なコアがEコアのみの場合はEコア)にシフトすることがあります。
およびスケジュール呼び出しによりyield
、setpri()
どの優先順位でスレッドが実行されるか、いつ他のタスクに譲るかが決定されます。
Appleプラットフォームで使用するとyield
、コアはシステム上で実行されている他のスレッドに処理を委譲することになります。この曖昧な動作定義は、Instrumentsで実行時に追跡が困難なパフォーマンスのボトルネックを引き起こす可能性があります。
yield
パフォーマンスはプラットフォームやOSによって異なり、最大10ミリ秒もの長い実行遅延を引き起こす可能性があります。yield
またはを使用するsetpri()
と、特定のCPUコアの実行速度が一時的にゼロになる可能性があるため、可能な限り
使用を避けてください。
また、sleep(0)
Apple プラットフォームでは - は意味を持たず、何も実行されないため、使用しないでください。
スケールのスレッド数
一般的に、CPUコア数に応じて適切な数のスレッドを使用する必要があります。コア数が少ないデバイスでスレッドを過剰に実行すると、パフォーマンスが低下する可能性があります。
スレッドが多すぎると、コストのかかるコアコンテキストスイッチが発生します。
スレッド数が少なすぎると、逆の問題が発生し、複数のコアでスケジュールするためにスレッドを並列化する機会が少なくなります。
ゲームの起動時には常に CPU 設計を照会して、どのような CPU 環境で実行されているか、使用可能なコアがいくつあるかを確認してください。
スレッド プールは、全体的なタスクのスレッド数ではなく、常に CPU コア数に基づいて拡張する必要があります。
ゲーム設計で特定のタスクに多数のワーカー スレッドが必要な場合でも、同時に実行するスレッドが多すぎてコアが少なすぎると、ゲームは効率的に実行されません。
UNIXsysctlbyname
関数を使用して、iOSまたはmacOSデバイスを照会できます。このhw.nperflevels
sysctlbyname
パラメータは、デバイスが搭載する一般的なCPUコアの数に関する情報を返します。
楽器を使う
Apple の Instruments アプリには、実行時にゲームのパフォーマンスを確認および測定するために使用できる ゲーム パフォーマンステンプレートがあります。
Apple の楽器。
Instrumentsには、スレッドの実行状態と待機状態をトレースできる スレッド状態トレース機能もあります。TSTを使用すると、どのスレッドがアイドル状態になり、その時間の長さを追跡できます。
まとめ
ゲームの最適化は非常に複雑なトピックであり、アプリのパフォーマンスを最大化するためのテクニックをいくつか紹介したに過ぎません。学ぶべきことはまだまだたくさんあります。このトピックをマスターするには数日かかることを覚悟してください。
多くの場合、Instruments を使用してコードの動作を追跡し、パフォーマンスのボトルネックが発生する箇所を修正することで、試行錯誤から最も効果的に学習できます。
全体として、マルチコア Apple システムでのゲームジョブのスケジューリングで留意すべき重要なポイントは次のとおりです。
- タスクをできるだけ小さくする
- できるだけ多くの小さなタスクを単一のスレッドにグループ化する
- スレッドのオーバーヘッド、スケジュール、同期を可能な限り削減する
- コアのアイドル/ウェイクサイクルを回避する
- スレッドコンテキストスイッチを避ける
- ジョブプーリングを使用する
- 必要なときだけスレッドを起動する
- 可能な場合はsleep(0)とyieldの使用を避ける
- スレッドシグナルにはセマフォを使用する
- スレッド数をCPUコア数に合わせて調整する
- 楽器を使う
Apple は、「Apple Silicon ゲーム用の CPU ジョブ スケジューリングの調整」というタイトルの WWDC ビデオも公開しており、上記のほとんどのトピックとその他多くのトピックについて説明しています。
ゲームコードのスケジュールの詳細に注意することで、Apple Silicon ゲームから最大限のパフォーマンスを引き出すことができます。