web-dev-qa-db-ja.com

複数のコンポーネントを処理するデータ指向のECS更新ループを作成する

したがって、次のような構造のエンジンが進行中です。

  • エンティティは単純なID(unsigned short)です
  • コンポーネントは静的メンバーであるプールに保存されます
  • システムはマネージャーによって保存され、すべて単一のベースから継承します

これはコードでは次のようになります。

struct transformComponent {
    const unsigned short id;
    vec2 pos, dim;

    transformComponent(vec2 pos, vec2 dim, unsigned short id): pos(pos), dim(dim), id(id) {}

    static componentPool<transformComponent> pool; // allows for scalability
};

struct physicsComponent {
    const unsigned short id;
    vec2 v, a, f;
    unsigned short mass;
    float invmass

    physicsComponent(vec2 v, vec2 a, vec2 f, unsigned short mass, unsigned short id): v(v), a(a), f(f), mass(mass), invmass(1.0/mass), id(id) {}

    static componentPool<physicsComponent> pool;
};

// same for other components

struct system {
    virtual void update() = 0; // don't care about virtual call, there will be only one per system
};

struct physicsSystem: public system {
    virtual void update() override; // the problem
};

struct room { // the manager
    std::vector<system*> systems;
    std::unordered_set<unsigned short> activeIds;

    void update() {for(auto* sys: systems) {sys->update();}}
};

今、私はこれまで、これを可能な限りキャッシュフレンドリーにするために、できる限りのことをしました。重要なのは、物理ループが物理コンポーネントを読み取って変換に書き込む場合、ポイントが完全になくなるのではないでしょうか。私はいくつかのコードに私の意味を説明させます:

/* either:
  physicsSystem has a hashmap of ids that it has to loop over to component pointers, which only gets updated when necessary (std::unordered_map<unsigned short, std::Tuple<transformComponent*, physicsComponent*>> comps;), breaks purity (systems have no state)
or
  comps is created every frame (for every system), terribly inefficient
*/

// either:
void physicsSystem::update() {
    for(auto pair: comps) {
        physicsComponent* phy = std::get<physicsComponent*>(pair.second);
        transformComponent* tra = std::get<transformComponent*>(pair.second);

        phy->a = phy->f * phy->invmass;
        phy->v += phy->a;
        tra->pos += phy->v;

        // cache misses to load both transform and physics twice, for every entity, for every system, for every frame
    }
}

// or:
// comps is actually an std::unordered_map<unsigned short, std::Tuple<transformComponent*, physicsComponent*, vec2>>
void physicsSystem::update() {
    for(auto pair: comps) {
        physicsComponent* phy = std::get<physicsComponent*>(pair.second);
        vec2& schedule = std::get<vec2>(pair.second);

        phy->a = phy->f * phy->invmass;
        phy->v += phy->a;
        schedule = phy->v;
    }
    for(auto pair: comps) {
        std::get<transformComponent*>(pair.second)->pos += std::get<vec2>(pair.second);
    }
    // smooth first loop, but still cache misses in the second (I think)
}

だから、私の質問は、このループをキャッシュフレンドリーにする方法は?つまり、すべてのエンティティのキ​​ャッシュミスはありませんが、配列のサイズがキャッシュを超えた場合と、コンポーネントタイプを切り替えて更新する場合に限られます。また、いろいろなアプローチをいただければ幸いです。 TIA。

1

つまり、キャッシュが実際にどのように機能するのかわからなかったことがわかりました。他の誰かが同じ頭痛を経験する必要がないことを期待して、これを書いています。
私の最初のアプローチは実際には問題ありません。はい、キャッシュは(通常は約)64バイト(アーキテクチャによって異なります)ですが、キャッシュ自体ははるかに大きいです(私の2016年のラップトップでは、L3は3メガです-チェックする) 、Windowsでは、タスクマネージャーを開きます->詳細->パフォーマンス)。

したがって、アドレス0x000001を要求した場合、バスはキャッシュアドレス0x000001から0x000040をもたらします。したがって、これらのバイトのいずれかを要求すると、それらはすでにキャッシュされており、自由に使用できます。
しかし、ループがアドレス0x000041を要求した場合、CPUはL1(最小かつ最速)を検索し、バイトが見つかればそれを提供します。それ以外の場合は、L2キャッシュに移動します(大きくて低速)。そして、それが見つからない場合は、L3(最大で最も遅い)に見えます。それでも見つからない場合は、RAMを要求します。そして今何が起こるのでしょうか?アドレス0x000041から0x000080が読み込まれますただし、他のアドレスは上書きされません。つまり、たとえばアドレス0x00000fを要求した場合でも、アドレスは引き続きキャッシュされ、準備が整います。

1