エンティティコンポーネントシステムのゲームエンジンでCPUキャッシュを活用するにはどうすればよいですか?


15

CPUキャッシュを賢く使用するための優れたアーキテクチャであるECSゲームエンジンのドキュメントをよく読みます。

しかし、CPUキャッシュの利点を理解することはできません。

コンポーネントが連続したメモリの配列(またはプール)に保存されている場合、コンポーネントを順番に読み取る場合にのみCPUキャッシュを使用するのが良い方法です。

システムを使用する場合、特定のタイプのコンポーネントを持つエンティティのリストであるエンティティリストが必要です。

ただし、これらのリストは、順番ではなくランダムな方法でコンポーネントを提供します。

それでは、キャッシュヒットを最大化するECSを設計する方法は?

編集:

たとえば、物理システムには、RigidBodyおよびTransformコンポーネントを持つエンティティのエンティティリストが必要です(RigidBodyのプールとTransformコンポーネントのプールがあります)。

したがって、エンティティを更新するためのループは次のようになります。

for (Entity eid in entitiesList) {
    // Get rigid body component
    RigidBody *rigidBody = entityManager.getComponentFromEntity<RigidBody>(eid);

    // Get transform component
    Transform *transform = entityManager.getComponentFromEntity<Transform>(eid);

    // Do something with rigid body and transform component
}

問題は、entity1のRigidBodyコンポーネントがそのプールのインデックス2にあり、entity1のTranformコンポーネントがそのプールのインデックス0にあることです(一部のエンティティは他のコンポーネントを持たず、エンティティを追加/削除するため/ランダムにコンポーネント)。

コンポーネントがメモリ内で連続している場合でも、それらはランダムに読み取られるため、キャッシュミスが多くなります。

ループ内の次のコンポーネントをプリフェッチする方法がない限り?


各コンポーネントをどのように割り当てているか教えてください。
concept3d

単純なプールアロケーターと、プール内のコンポーネントの再配置を管理するためのコンポーネント参照を持つためのハンドルマネージャーを使用して(コンポーネントをメモリ内で連続させます)。
ジョンフ

ループの例では、コンポーネントの更新はエンティティごとにインターリーブされると想定しています。多くの場合、コンポーネントタイプごとにコンポーネントを一括更新できます(たとえば、最初にすべてのRigidbodyコンポーネントを更新し、次に完成したRigidbodyデータですべてのトランスフォームを更新し、次に新しいトランスフォームですべてのレンダリングデータを更新します)-これによりキャッシュを改善できます各コンポーネントの更新に使用します。このタイプの構造は、ニック・ウィギルが下で提案しているものだと思います。
DMGregory

悪いのは私の例ですが、実際には、物理​​システムよりも「完成した剛体データですべての変換を更新する」システムです。しかし、問題は同じままです。これらのシステムでは(剛体で変換を更新、変換でレンダリングを更新、...)、同時に複数のタイプのコンポーネントが必要になります。
ジョンフ

これも関連性があるかどうかわかりませんか?gamasutra.com/view/feature/6345/...
DMGregory

回答:


13

Mick Westの記事では、エンティティコンポーネントデータを完全に線形化するプロセスについて説明しています。何年も前のTony Hawkシリーズでは、現在よりもはるかに印象的なハードウェアでパフォーマンスを大幅に向上させることができました。彼は基本的に、エンティティデータの各タイプ(位置、スコア、その他)に事前に割り当てられたグローバルな配列を使用し、システム全体のupdate()機能の異なるフェーズで各配列を参照しました。各エンティティのデータは、これらの各グローバル配列の同じ配列インデックスにあると想定できます。したがって、たとえば、プレーヤーが最初に作成された場合、[0]各配列にデータがある可能性があります。

キャッシュの最適化にさらに具体的な、CおよびC ++向けのChrister Ericssonのスライド

もう少し詳しく説明するには、データの各タイプ(位置、xy、zなど)ごとに連続したメモリブロック(配列として最も簡単に割り当てられる)を使用して、参照の良好な局所性を確保し、そのような各データブロックを別個に使用するようにしてくださいupdate()一時的な局所性のためのフェーズ。つまり、特定のupdate()呼び出し内で、再利用するデータを再利用する前に、ハードウェアのLRUアルゴリズムを介してキャッシュがフラッシュされないようにします。あなたが暗示したように、あなたがしたくないことはnew、を介してエンティティとコンポーネントを個別のオブジェクトとして割り当てることです。各エンティティインスタンスの異なるタイプのデータはインターリーブされ、参照の局所性を減らします。

コンポーネント(データ)間に相互依存関係があり、関連データ(例:Transform + Physics、Transform + Renderer)からデータを分離する余裕がない場合、PhysicsおよびRenderer配列の両方でTransformデータを複製することを選択できます。 、すべての関連データがパフォーマンス重視の各操作のキャッシュライン幅に適合することを保証します。

また、L2およびL3キャッシュ(ターゲットプラットフォームでこれらを想定できる場合)は、線幅の制限など、L1キャッシュが被る可能性のある問題を軽減するために多くのことを行うことを忘れないでください。したがって、L1ミスでも、これらはほとんどの場合、メインメモリへのコールアウトを防ぐセーフティネットです。これは、あらゆるレベルのキャッシュへのコールアウトよりも桁違いに遅くなります。

データの書き込みに関する注意事項書き込みでは、メインメモリは呼び出されません。デフォルトでは、今日のシステムではライトバックキャッシュが有効になっています。値を書き込むと、メインメモリではなくキャッシュに(最初に)書き込まれるため、これによってボトルネックになることはありません。メインメモリからデータが要求され(キャッシュにある間は発生しません)、古い場合のみ、メインメモリはキャッシュから更新されます。


1
C ++を初めて使用する場合のstd::vector基本的な注意事項は、基本的に動的にサイズ変更可能な配列であり、連続していることです(古いC ++バージョンでは事実上、新しいC ++バージョンではデジュールです)。の一部の実装std::dequeも「十分に連続」しています(ただし、Microsoftの実装はそうではありません)。
ショーンミドルディッチ

2
@Johnmph簡単に言うと、参照の場所がなければ、何もありません。2つのデータが密接に関連している場合(空間情報と物理情報など)、つまり、それらが一緒に処理される場合、それらをインターリーブされた単一のコンポーネントとして圧縮する必要があります。ただし、その空間データを活用する他のロジック(AIなど)は、空間データが含まれていないために影響を受ける可能性があることに注意してください。そのため、最もパフォーマンスが必要なものに依存します(おそらく物理学)。それは理にかなっていますか?
エンジニア

1
@Johnmphはい、ニックに完全に同意します。メモリにどのように保存されるかについてです。メモリ内の遠く離れた2つのコンポーネントへのポインタを持つエンティティがあり、ローカリティがない場合は、キャッシュラインに収まる必要があります。
concept3d

2
@Johnmph:確かに、Mick Westの記事は最小限の相互依存関係を想定しています。そのため、依存関係を最小限に抑えます。これらの依存関係を最小化できないキャッシュラインに沿ってデータを複製します。たとえば、RigidBody Renderの両方にTransformを含めます。また、キャッシュラインに合わせるために、データアトムを可能な限り減らす必要があります...これは、小数点値ごとに浮動小数点から固定小数点(4バイト対2バイト)に移行することで部分的に達成できます。ただし、パフォーマンスを最大化するには、何らかの方法で、concept3dに記載されているように、データをキャッシュライン幅に合わせる必要があります。
エンジニア

2
@Johnmph。いいえ。変換データを書き込むときは常に、両方の配列に書き込むだけです。心配する必要のある書き込みではありません。書き込みを送信すると、完了と同じくらい良好です。それはだ読み取っあなたは物理学とレンダラを実行するときに、後にアップデートで、必要があります右のアンカーウーマンCPUへの単一のキャッシュラインに、すぐに、すべての関連データへのアクセス権を持っています。また、本当に必要な場合は、さらに複製を行うか、物理、変換、レンダリングを1つのキャッシュラインに合わせます。64バイトが一般的で、実際には大量のデータです!...
エンジニア
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.