エンティティシステムのキャッシュ効率はどのようになっていますか?


32

最近、私はC ++ / OpenGLゲームエンジンに実装するために、エンティティシステムで多くの読書を行ってきました。エンティティシステムについて絶賛されている2つの主な利点は次のとおりです。

  1. 複雑な継承階層に絡む必要がないため、新しい種類のエンティティを簡単に構築できます。
  2. キャッシュの効率性、私は理解するのに苦労しています。

理論はもちろん単純です。各コンポーネントはメモリブロックに連続して格納されるため、そのコンポーネントを処理するシステムは、メモリ内をジャンプしてキャッシュを削除することなく、リスト全体を繰り返し処理できます。問題は、これが実際に実用的である状況を本当に考えることができないということです。


最初に、コンポーネントがどのように保存され、どのように相互に参照するかを見てみましょう。システムは複数のコンポーネントで動作する必要があります。つまり、レンダリングシステムと物理システムの両方が変換コンポーネントにアクセスする必要があります。私はこれに対処する多くの可能な実装を見てきましたが、どれもうまくいきません。

コンポーネントに、他のコンポーネントへのポインター、またはコンポーネントへのポインターを格納するエンティティへのポインターを格納させることができます。ただし、ポインタをミックスに投入するとすぐに、キャッシュの効率がすでに低下しています。すべてのコンポーネント配列が「n」の大きさであることを確認できます。「n」はシステム内に存在するエンティティの数ですが、このアプローチはひどくメモリを浪費します。これにより、新しいコンポーネントタイプをエンジンに追加することは非常に困難になりますが、1つのアレイから次のアレイにジャンプするため、キャッシュ効率が失われます。個別の配列を保持する代わりに、エンティティ配列をインターリーブすることもできますが、それでもメモリを浪費しています。新しいコンポーネントやシステムを追加するのに法外な費用がかかりますが、古いレベルをすべて無効にしてファイルを保存するという利点が追加されました。

これはすべて、エンティティがリスト、すべてのフレームまたはティックで線形に処理されることを前提としています。実際には、これは多くの場合そうではありません。オクルージョンカリングを実行するためにセクター/ポータルレンダラー、またはオクトツリーを使用するとします。エンティティをセクター/ノード内に連続して保存できる場合もありますが、好きかどうかに関係なく、飛び回ることになります。次に、他のシステムがあります。これは、他の何らかの順序で格納されたエンティティを好む場合があります。AIは、AI LODでの作業を開始するまで、エンティティを大きなリストに格納しても問題ありません。次に、プレーヤーまでの距離、またはその他のLODメトリックに従って、そのリストを分割します。物理学はそのオクトツリーを使用したいと思うでしょう。スクリプトは何でも構いません。実行する必要があります。

コンポーネントを「ロジック」(ai、スクリプトなど)と「ワールド」(レンダリング、物理、オーディオなど)に分割し、各リストを個別に管理できますが、これらのリストは相互にやり取りする必要があります。AIは、エンティティのレンダリングに使用される変換またはアニメーションの状態に影響を与えない場合、意味がありません。


エンティティシステムは、実際のゲームエンジンでどのように「キャッシュ効率」が高いのですか?おそらく、アレイにエンティティをグローバルに保存し、octree内でそれを参照するなど、誰もが使用しているが、話しているわけではないハイブリッドアプローチがあるのでしょうか。


最近、マルチコアCPUとキャッシュが1行よりも大きいことに注意してください。2つのシステムからのアクセス情報が必要な場合でも、両方に適合する可能性があります。また、グラフィックレンダリングは、多くの場合分離されています-まさにあなたが述べたもの(木、シーン、..)
ウォンドラ14

2
エンティティシステムは、常にキャッシュ効率が高いとは限りません、一部の実装の利点になる場合があります(同様のことを達成する他の方法よりも)。
ジョシュ

回答:


43

エンティティシステムについて絶賛されている2つの主な利点は、1)複雑な継承階層に絡む必要がないため、新しい種類のエンティティを簡単に構築できること、および2)キャッシュ効率です。

(1)は、ES / ECSだけでなく、コンポーネントベースの設計の利点であることに注意してください。「システム」部分を持たない多くの方法でコンポーネントを使用でき、それらはうまく機能します(そして、多くのインディーゲームとAAAゲームの両方がそのようなアーキテクチャを使用しています)。

標準のUnityオブジェクトモデル(使用GameObjectおよびMonoBehaviourオブジェクト)はECSではなく、コンポーネントベースの設計です。もちろん、新しいUnity ECS機能は実際のECSです。

システムは複数のコンポーネントで動作する必要があります。つまり、レンダリングシステムと物理システムの両方が変換コンポーネントにアクセスする必要があります。

一部のECSは、エンティティIDでコンポーネントコンテナーをソートします。つまり、各グループの対応するコンポーネントは同じ順序になります。

これは、グラフィックスコンポーネントを線形に反復している場合、対応する変換コンポーネント線形に反復していることを意味します。いくつかの変換をスキップしている可能性があります(レンダリングしない物理トリガーボリュームがあるなどの理由で)が、メモリ内で常に前方スキップしているため(通常、特に大きな距離ではないため)効率が向上します。

これは、HPCの推奨されるアプローチである構造の配列(SOA)に似ています。CPUとキャッシュは、単一の線形配列を処理できるのとほぼ同じように、複数の線形配列を処理でき、ランダムメモリアクセスを処理するよりもはるかに優れています。

Unity ECSを含むいくつかのECS実装で使用される別の戦略は、対応するエンティティのアーキタイプに基づいてコンポーネントを割り当てることです。つまり、コンポーネントの正確セットを持つすべてのエンティティは、( 、PhysicsBodyTransform異なるコンポーネントを持つエンティティとは別に割り当てられます(たとえばPhysicsBodyTransformおよび Renderable)。

そのような設計のシステムは、最初に要件に一致するすべてのアーキタイプ(コンポーネントの必要なセットを含む)を見つけ、そのアーキタイプのリストを繰り返し、一致する各アーキタイプ内に格納されたコンポーネントを繰り返します。これにより、アーキタイプ内で完全に線形かつ真のO(1)コンポーネントアクセスが可能になり、システムは非常に低いオーバーヘッドで互換性のあるエンティティを見つけることができます(潜在的に数十万のエンティティを検索するのではなく、アーキタイプの小さなリストを検索することにより)。

コンポーネントに、他のコンポーネントへのポインター、またはコンポーネントへのポインターを格納するエンティティへのポインターを保存させることができます。

同じエンティティ上の他のコンポーネントを参照するコンポーネントは、何も保存する必要はありません。他のエンティティ上のコンポーネントを参照するには、エンティティIDを保存するだけです。

単一のエンティティに対してコンポーネントが複数回存在することが許可されており、特定のインスタンスを参照する必要がある場合、他のエンティティのIDとそのエンティティのコンポーネントインデックスを保存します。ただし、特にこれらの操作の効率が低下するため、多くのECS実装ではこのケースを許可していません。

すべてのコンポーネント配列が「n」の大きさであることを確認できます。「n」はシステム内に存在するエンティティの数です

ポインターではなくハンドル(インデックス+生成マーカーなど)を使用すると、オブジェクト参照を壊す心配なく配列のサイズを変更できます。

std::deque何らかの理由でポインタを許可したい場合や、問題を測定した場合は、多くの一般的な実装に似た「チャンク配列」アプローチ(配列の配列)を使用できます(ただし、実装の哀れなほど小さいチャンクサイズはありません)配列のサイズ変更のパフォーマンス。

第二に、これはすべてのエンティティがリストでフレーム/ティックごとに線形に処理されることを前提としていますが、実際にはそうではありません

エンティティに依存します。はい、多くのユースケースでは、そうではありません。実際、これが、コンポーネントベースの設計(良い)とエンティティシステム(特定の形式のCBD)の違いを非常に強調している理由です。

一部のコンポーネントは確かに直線的に簡単に処理できます。通常、「ツリーが重い」ユースケースでも、密集したアレイを使用することでパフォーマンスが確実に向上します(ほとんどの場合、典型的なゲームのAIエージェントのように、最大​​で数百のNを含むケース)。

一部の開発者は、データ指向の線形に割り当てられたデータ構造を使用することのパフォーマンスの利点が、「スマートな」ツリーベースの構造を使用することのパフォーマンスの利点を上回ることも発見しました。もちろん、すべてはゲームと特定のユースケースに依存します。

セクター/ポータルレンダラーまたはoctreeを使用してオクルージョンカリングを実行するとします。エンティティをセクター/ノード内に連続して保存できる場合もありますが、好きかどうかに関係なく飛び回ることになります。

アレイがどれだけ役立つのか驚くでしょう。「どこでも」よりもはるかに小さなメモリ領域でジャンプしているのに、ジャンプしたとしてもキャッシュ内の何かになってしまう可能性がずっと高くなります。特定のサイズ以下のツリーでは、すべてをキャッシュにプリフェッチでき、そのツリーでキャッシュミスが発生することはありません。

また、密集した配列に収まるように構築されたツリー構造もあります。たとえば、octreeでは、ヒープのような構造(子の前に親、隣同士に兄弟)を使用して、ツリーを「ドリルダウン」しても、常に前方に反復するようにできます。 CPUはメモリアクセス/キャッシュルックアップを最適化します。

これは重要なポイントです。x86 CPUは複雑な獣です。CPUは、マシンコードでマイクロコードオプティマイザーを効果的に実行し、小さなマイクロコードに分割して命令を並べ替え、メモリアクセスパターンなどを予測します。 CPUまたはキャッシュの仕組み。

次に、他のシステムがあります。これは、他の何らかの順序で格納されたエンティティを好む場合があります。

それらを複数回保存できます。配列を最小限の詳細まで削除すると、この方法で実際にメモリを節約できることがわかります(64ビットポインターを削除し、より小さいインデックスを使用できるため)。

個別の配列を維持する代わりにエンティティ配列をインターリーブすることもできますが、それでもメモリを無駄にしています

これは、良好なキャッシュ使用量とは正反対です。変換とグラフィックスのデータだけが必要な場合、物理学とAI、入力とデバッグなどの他のすべてのデータの取り込みに時間を費やすのはなぜですか?

これは通常、ECSとモノリシックゲームオブジェクトを優先する点です(ただし、他のコンポーネントベースのアーキテクチャと比較した場合、実際には適用できません)。

価値があるものとして、私が知っているほとんどの「生産グレード」のECS実装は、インターリーブストレージを使用します。前述の一般的なArchetypeのアプローチ(Unity ECSなどで使用)は、Archetypeに関連付けられたコンポーネントにインターリーブストレージを使用するように非常に明示的に構築されています。

AIは、エンティティのレンダリングに使用される変換またはアニメーションの状態に影響を与えない場合、意味がありません。

AIが変換データに直線的に効率的にアクセスできないからといって、他のシステムがそのデータレイアウトの最適化を効果的に使用できないというわけではありません。ゲームロジックシステムが通常行うようなアドホックな方法でゲームロジックシステムが行うことを止めることなく、パックされた配列を使用してデータを変換できます。

また、コードキャッシュを忘れています。ECSのシステムアプローチを使用する場合(より単純なコンポーネントアーキテクチャとは異なり)、同じ小さなループループを実行し、仮想関数テーブルを前後に行き来するランダムUpdate関数の種類にジャンプしてジャンプしないように保証します。あなたのバイナリ。そのため、AIの場合、さまざまなAIコンポーネント(ビヘイビアを構成できるように複数のコンポーネントがあるのは確かです!)を別々のバケットに保持し、各リストを個別に処理して、コードキャッシュの使用を最適化する必要があります。

遅延イベントキュー(システムはイベントのリストを生成しますが、システムがすべてのエンティティの処理を完了するまでディスパッチしません)を使用すると、イベントを保持しながらコードキャッシュを適切に使用できます。

各システムがフレームのどのイベントキューを読み取るかを知っているアプローチを使用すると、イベントの読み取りを高速化することもできます。または、少なくともなしでよりも高速です。

パフォーマンスは絶対的なものではありません。優れたデータ指向設計のパフォーマンス上の利点を確認するために、最後の単一のキャッシュミスをすべて排除する必要はありません。

ECSアーキテクチャおよびデータ指向のデザインパターンで多くのゲームシステムをより良く機能させるための研究がまだ活発に行​​われています。近年、SIMDで行った驚くべきこと(JSONパーサーなど)のいくつかと同様に、古典的なゲームアーキテクチャには直観的ではないが、多くの利点(速度、マルチスレッド、テスト容易性など)。

または、誰もが使用しているが誰も話していないハイブリッドアプローチがあるかもしれません

これは、特にECSアーキテクチャに懐疑的な人々にとって、私が過去に提唱したものです。パフォーマンスが重要なコンポーネントには、優れたデータ指向のアプローチを使用してください。シンプルさが開発時間を短縮する、よりシンプルなアーキテクチャを使用します。ECSが提案するように、すべてのコンポーネントを厳密にコンポーネント化の過剰定義に陥らせないでください。ECSのようなアプローチが理にかなっている場合は簡単に使用できるようにコンポーネントアーキテクチャを開発し、ECSのようなアプローチが理にかなっていない(またはツリー構造よりも理にかなっていない)単純なコンポーネント構造を使用する。

私は個人的には、ECSの真の力への比較的最近の改宗者です。私にとっては、決定要因はECSについてめったに言及されていませんでした:ゲームシステムとロジックのテストを書くことは、私が過去に取り組んだ緊密に結合されたロジックを搭載したコンポーネントベースのデザインと比較してほとんど些細なことです。ECSアーキテクチャはすべてのロジックをシステムに配置し、コンポーネントを消費してコンポーネントの更新を生成するだけなので、システムの動作をテストするためのコンポーネントの「模擬」セットを構築するのは非常に簡単です。ほとんどのゲームロジックはシステム内にのみ存在する必要があるため、事実上、すべてのシステムをテストすると、ゲームロジックのコードカバレッジがかなり高くなります。システムは、テストよりも複雑さやパフォーマンスへの影響がはるかに少ないテストのために、モックの依存関係(GPUインターフェイスなど)を使用できます。

余談ですが、多くの人がECSのことを本当に理解せずにECSについて話していることに気付くかもしれません。多くのゲーム開発者が「ECS」を「Components」と同等であり、「Entity System」の部分を完全に無視していることを示す、古典的なUnityはECSと呼ばれます。インターネット上のECSには、実際のECSではなく、コンポーネントベースのデザインを支持している人が大勢いるときに、多くの愛が山積しています。この時点で、それを議論することはほとんど無意味です。ECSは元の意味から一般的な用語に破損しているため、「ECS」は「データ指向のECS」と同じ意味ではないことを受け入れてください。:/


1
一般的なコンポーネントベースの設計と比較/対照する場合は、ECSの意味を定義(またはリンク)すると便利です。私は、その区別が何であるか明確ではありません。:)
ネイサンリード14

答えてくれてありがとう、私はまだこのテーマに関してやるべきことがたくさんあるようです。あなたが私を指すことができる本はありますか?
ハイドンV.ハラッハ14

3
@NathanReed:ECSentity-systems.wikidot.com/es-terminologyのような場所で文書化されています。コンポーネントベースの設計は、通常の継承による集約に過ぎませんが、ゲーム設計に役立つ動的な構成に重点を置いています。システムやエンティティ(ECSの用語では)を使用しないコンポーネントベースのエンジンを記述できます。また、ゲームオブジェクト/エンティティだけでなく、ゲームエンジンでコンポーネントをはるかに多く使用できるため、違いを強調しています。
ショーンミドルディッチ14

2
これは、ウェブに関するすべての文献にもかかわらず、私が今まで読んだECSに関する最高の投稿の1つです。メガ親指。ショーン、最終的に、ゲームを開発するための一般的なアプローチは(複雑なゲームではなく)純粋なECSですか?コンポーネントベースとECSの混合アプローチ?あなたのデザインについてもっと知りたいです!これについて話し合うために、Skypeであなたをつかまえたり、何か他のものを求めたりしますか?
グリムショー14

2
@Grimshaw:gamedev.netは、私が思うreddit.com/r/gamedevのように、より自由な議論のためのまともな場所です(私は自分自身ではありませんが)。私は頻繁にgamedev.netにいます。他の多くの優秀な人々もそうです。通常、1対1の会話はしません。私はかなり忙しく、ダウンタイム(つまり、コンパイル)が少数ではなく多くの人を助けるために費やされることを好みます。:)
ショーン・ミドルディッチ14
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.