エンティティ/コンポーネントシステムでエンティティを同時に処理するための読み取り/計算/書き込みステップを効率的に分離


11

セットアップ

エンティティコンポーネントアーキテクチャがあり、エンティティは一連の属性(動作のない純粋なデータ)を持つことができ、そのデータに作用するエンティティロジックを実行するシステムが存在します。基本的に、やや疑似コードで:

Entity
{
    id;
    map<id_type, Attribute> attributes;
}

System
{
    update();
    vector<Entity> entities;
}

すべてのエンティティに沿って一定の速度で移動するシステムは、

MovementSystem extends System
{
   update()
   {
      for each entity in entities
        position = entity.attributes["position"];
        position += vec3(1,1,1);
   }
}

基本的に、私はupdate()をできるだけ効率的に並列化しようとしています。これは、システム全体を並行して実行するか、1つのシステムの各update()にいくつかのコンポーネントを与えることにより、異なるシステムが同じシステムの更新を実行できるようにして、そのシステムに登録されたエンティティの異なるサブセットに対して実行できます。

問題

示されているMovementSystemの場合、並列化は簡単です。エンティティは相互に依存せず、共有データを変更しないため、すべてのエンティティを並行して移動できます。

ただし、これらのシステムでは、エンティティが相互にやり取りする(データを読み書きする)ことが必要な場合があります。同じシステム内でも、相互に依存する異なるシステム間である場合がよくあります。

たとえば、物理システムでは、エンティティが互いに相互作用する場合があります。2つのオブジェクトが衝突し、それらの位置、速度、およびその他の属性がオブジェクトから読み取られて更新され、更新された属性が両方のエンティティに書き戻されます。

エンジンのレンダリングシステムがエンティティのレンダリングを開始する前に、他のシステムが実行を完了するのを待って、関連するすべての属性が必要な属性であることを確認する必要があります。

これを盲目的に並列化しようとすると、異なるシステムが同時にデータを読み取り、変更する可能性がある従来の競合状態が発生します。

理想的には、他のシステムが同じデータを同時に変更することを心配することなく、またプログラマが実行と並列化を適切に順序付けすることを気にすることなく、すべてのシステムが必要なエンティティからデータを読み取ることができるソリューションが存在しますこれらのシステムを手動で(場合によっては不可能になることもあります)。

基本的な実装では、これはすべてのデータの読み取りと書き込みをクリティカルセクションに配置するだけで実現できます(ミューテックスで保護します)。しかし、これは大量のランタイムオーバーヘッドを引き起こし、パフォーマンスに敏感なアプリケーションにはおそらく適していません。

解決?

私の考えでは、考えられる解決策は、データの読み取り/更新と書き込みが分離されているシステムであり、1つの高価なフェーズでは、システムはデータの読み取りと計算に必要なものだけを計算し、何らかの方法で結果をキャッシュしてからすべて書き込みます。別の書き込みパスで変更されたデータをターゲットエンティティに戻します。すべてのシステムは、フレームの先頭にある状態でデータに作用し、その後、フレームの終わりの前に、すべてのシステムが更新を完了すると、シリアル化された書き込みパスが発生し、すべての異なるキャッシュ結果がキャッシュされますシステムは反復され、ターゲットエンティティに書き戻されます。

これは、簡単な並列化の勝利が結果のキャッシュと書き込みパスのコスト(実行時のパフォーマンスとコードのオーバーヘッドの両方の点で)を上回るほど大きくなる可能性がある(多分間違っているか?)という考えに基づいています。

質問

最適なパフォーマンスを実現するために、このようなシステムをどのように実装するのでしょうか?そのようなシステムの実装の詳細と、このソリューションを使用するエンティティコンポーネントシステムの前提条件は何ですか?

回答:


1

-----(改訂された質問に基づく)

最初のポイント:リリースビルドランタイムのプロファイルを作成し、特定のニーズを見つけたとは言わないので、できるだけ早く行うことをお勧めします。プロファイルはどのように見えますか?メモリレイアウトが悪いキャッシュをスラッシングしていますか、1つのコアが100%で固定されているか、ECSの処理と残りのエンジンの処理に費やされる相対時間などです...

エンティティから読み取り、何かを計算します...そして、中間のストレージ領域のどこかに結果を保持しますか?私は、この中間ストアが純粋なオーバーヘッド以外のものであると考え、期待する方法でread + compute + storeを分離できるとは思いません。

さらに、継続的な処理を実行しているため、従うべき主なルールは、CPUコアごとに1つのスレッドを持つことです。あなたはこれを間違った層で見ていると思います。個々のエンティティではなくシステム全体を見てみてください。

システム間の依存関係グラフを作成します。これは、以前のシステムの作業の結果である、システムが必要とするツリーです。依存関係ツリーを取得したら、エンティティでいっぱいのシステム全体をスレッドで処理するために簡単に送信できます。

それでは、依存関係ツリーがキイチゴとクマのわなの泥沼であり、設計上の問題であるとしましょう。ここでの最良のケースは、各システム内で各エンティティがそのシステム内の他の結果に依存しないことです。ここでは、このシステムが所有する2つのコアと200のエンティティの例として、2つのスレッドで0〜99および100〜199のスレッド間で処理を簡単に分割できます。

どちらの場合も、各ステージで、次のステージが依存する結果を待つ必要があります。しかし、10個の大きなデータブロックがまとめて処理されるという結果を待つことは、小さなブロックに対して1000回同期するよりもはるかに優れているため、これは問題ありません。

依存関係グラフの作成の背後にある考えは、自動化することで、「他のシステムを見つけて組み立てて並列実行する」という一見不可能に見えるタスクを簡単にすることでした。そのようなグラフが前の結果を常に待つことによってブロックされている兆候を示している場合は、読み取り+変更と遅延書き込みを作成すると、ブロックが移動するだけで、処理のシリアル性は削除されません。

また、シリアル処理は各シーケンスポイント間でのみ並列化できますが、全体的にはできません。しかし、それが問題の核心なので、これに気付きます。まだ書き込まれていないデータからの読み取りをキャッシュしても、キャッシュが利用可能になるまで待つ必要があります。

並列アーキテクチャの作成が簡単で、これらの種類の制約があっても可能であれば、コンピューターサイエンスはブレッチリーパーク以来、問題に苦しんでいなかったでしょう。

唯一の実際の解決策は、これらの依存関係すべて最小限にして、シーケンスポイントをできるだけ必要としないようにすることです。これには、システムを順次処理ステップに細分することが含ま、各サブシステム内で、スレッドとの並列処理が簡単になります。

最高の私はこの問題を解決しました。レンガの壁に頭をぶつけると痛い場合は、小さなレンガの壁に頭をぶつけて、すねのみを打つようにすることをお勧めします。


申し訳ありませんが、この回答は非生産的なもののようです。私が探しているものが存在しないことを言っているだけです。これは論理的に間違っているようです(少なくとも原則として)。また、以前にいくつかの場所でそのようなシステムについて人々がほのめかしているのを見たことがありますただし、これはこの質問をする主な動機です)。ただし、元の質問では詳細が不十分だった可能性があります。そのため、大幅に更新しました(また、何かが気になったら更新し続けます)。
TravisG

また、意図的な攻撃はありません:P
TravisG

@TravisG Patrickが指摘したように、他のシステムに依存するシステムがよくあります。フレームの遅延を回避するため、または論理ステップの一部として複数の更新パスを回避するために、受け入れられる解決策は、更新フェーズをシリアル化し、可能な場合はサブシステムを並列で実行し、依存関係を持つサブシステムをすべてシリアル化する一方で、それぞれの内部で小さな更新パスをバッチ処理することです。 parallel_for()コンセプトを使用したサブシステム。これは、サブシステム更新パスのニーズと最も柔軟な組み合わせの理想的です。
ナロス2013

0

この問題の興味深い解決策を聞いたことがあります。つまり、エンティティデータのコピーが2つあるという考えです(無駄ですが、知っています)。1つは現在のコピーで、もう1つは過去のコピーです。現在のコピーは厳密に書き込み専用であり、過去のコピーは厳密に読み取り専用です。システムは同じデータ要素に書き込みたくないと思いますが、そうでない場合、それらのシステムは同じスレッド上にある必要があります。各スレッドはデータの相互に排他的なセクションの現在のコピーへの書き込みアクセス権を持ち、すべてのスレッドはデータの過去のすべてのコピーへの読み取りアクセス権を持っているため、過去のコピーのデータを使用して現在のコピーを更新することができます。ロッキング。各フレーム間では、現在のコピーが過去のコピーになりますが、ロールの交換を処理したいとします。

また、この方法では、すべてのシステムが、システムが処理する前後に変更されない古い状態で動作するため、競合状態が解消されます。


それがジョンカーマックのヒープコピートリックですよね。私はそれについて疑問に思いましたが、複数のスレッドが同じ出力場所に書き込む可能性があるという同じ問題がまだ潜在的にあります。すべてを「シングルパス」に保つ場合、これはおそらく良い解決策ですが、それがどれほど可能かはわかりません。
TravisG

画面表示レイテンシへの入力は、GUIの反応性を含めて、1フレームの時間で増加します。これは、アクション/タイミングゲーム、またはRTSのような重いGUI操作で問題になる場合があります。しかし、私はそれを創造的なアイデアとして気に入っています。
Patrick Hughes

私はこれを友人から聞いた、そしてそれがカーマックのトリックであることを知らなかった。レンダリングの方法によっては、コンポーネントのレンダリングが1フレーム遅れることがあります。これを更新フェーズに使用し、すべてが最新の状態になったら、現在のコピーからレンダリングできます。
ジョンマクドナルド

0

データの並列処理を処理する3つのソフトウェア設計を知っています。

  1. データを順次処理する:複数のスレッドを使用してデータを処理したいので、これは奇妙に聞こえるかもしれません。ただし、ほとんどのシナリオでは、他のスレッドが待機したり長時間実行される操作を行っている間に作業を完了するために、複数のスレッドが必要です。最も一般的な使用法は、単一のスレッドでユーザーインターフェイスを更新するUIスレッドですが、他のスレッドはバックグラウンドで実行される可能性がありますが、UI要素に直接アクセスすることはできません。バックグラウンドスレッドから結果を渡すために、次の妥当な機会に単一スレッドによって処理されるジョブキューが使用されます
  2. データアクセスの同期:これは、同じデータにアクセスする複数のスレッドを処理する最も一般的な方法です。ほとんどのプログラミング言語は、データが複数のスレッドによって同時に読み書きされるセクションをロックするために、クラスとツールを組み込んでいます。ただし、操作をブロックしないように注意する必要があります。一方、この方法では、リアルタイムアプリケーションで多くのオーバーヘッドが発生します。
  3. 同時変更は、発生時にのみ処理します。この楽観的なアプローチは、衝突がほとんど発生しない場合に実行できます。複数のアクセスがまったくなかった場合、データは読み取られて変更されますが、データが同時に更新されたときにそれを検出するメカニズムがあります。その場合、単一の計算が成功するまで再度実行されます。

エンティティシステムで使用できる各アプローチの例を以下に示します。

  1. コンポーネントCollisionSystemを読み取りPositionRigidBody更新する必要があるを考えてみましょうVelocity。はをVelocity直接操作CollisionSystemする代わりに、CollisionEventをの作業キューに入れますEventSystem。このイベントは、の他の更新とともに順次処理されますVelocity
  2. アンはEntitySystemそれを読み書きするために必要なコンポーネントのセットを定義します。それぞれEntity読み取りたい各コンポーネントの読み取りロックと、更新したい各コンポーネントの書き込みロックを取得します。このEntitySystemように、更新操作が同期されている間、すべてが同時にコンポーネントを読み取ることができます。
  3. の例ではMovementSystemPositionコンポーネントは不変であり、リビジョン番号が含まれています。MovementSystemsavely読み込みPositionおよびVelocityコンポーネントを、新たな計算をPosition読み取りインクリメント、リビジョン番号と更新試行Positionコンポーネントを。同時変更の場合、フレームワークは更新時にこれを示し、Entityによって更新される必要があるエンティティのリストに戻されMovementSystemます。

システム、エンティティ、および更新間隔に応じて、それぞれのアプローチは良い場合も悪い場合もあります。エンティティシステムフレームワークでは、ユーザーがこれらのオプションから選択してパフォーマンスを微調整できる場合があります。

ディスカッションにいくつかのアイデアを追加できるといいのですが、何かニュースがありましたらお知らせください。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.