如何在c++中实现与抽象类指针向量的缓存一致性



我正在用c++做一个小游戏。我在StackExchange网站上找到了关于缓存一致性的答案,我想在我的游戏中使用它,但我使用抽象类实体的子类。

我将所有实体存储在std::vector中,这样我就可以在循环中访问虚函数。Entity::update()是Entity的虚函数,它被子类覆盖,比如PlayerEntity。

在Game.hpp - Private Member Variables:

std::vector<Entity*> mEntities;
PlayerEntity* mPlayer;

在Game.cpp - Constructor:

mPlayer = new PlayerEntity();
mEntities.push_back(mPlayer);

我的update函数(在主循环中)是这样的:

void Game::update() {
  for (Entity* entity : mEntities) {
    entity->update(mTimeStep, mGeneralClock.getElapsedTime().asMilliseconds());
  }
}

我的问题是:我如何使我的实体对象在内存中彼此相邻,从而实现缓存一致性?我试图简单地使指针的向量成为对象的向量,并进行适当的更改,但是由于显而易见的原因,我不能使用多态性。侧面问题:什么决定了一个对象在内存中分配的位置?我整件事都做错了吗?如果是这样,我应该如何存储我的实体?

注意:如果我的英语不好,我很抱歉,我的母语不是英语。

显然,首先衡量哪些部分甚至值得优化。并非所有游戏都是一样的,游戏中的代码也不是一样的。完全重组触发最终boss死亡动画的脚本,使其使用1条缓存线而不是2条是没有意义的。也就是说…

如果你的目标是优化缓存,忘记继承和虚函数。或者至少批评他们。正如你所注意到的,创建一个连续的多态对象数组是很难的。容易出错且完全不可行的(取决于子类是否有不同的大小)。

您可以尝试创建一个池,使附近的实体(在实体向量中)更有可能彼此靠近(在内存中),但坦率地说,我怀疑您会比最先进的通用分配器做得更好,特别是当实体的大小和生命周期变化很大时。只有当向量中相邻的实体被连续分配时,池才有帮助。但是在这种情况下,任何标准分配器都具有相同的局部性优势。它不像tcmalloc和朋友们随机选择一个缓存线来分配只是为了惹恼你。

您可能能够从了解对象类型中挤出一点内存,但这纯粹是假设的,必须首先证明实现它的努力是合理的。还要注意,运行磨矿池要么假设所有对象大小相同,要么永远不会释放单个对象。同时允许这两种情况会使您朝着通用分配器迈进一半,而您肯定会做得更差。

您可以根据对象的类型隔离对象。也就是说,不再只有一个具有虚函数的多态Entity的向量,而是有N个向量:vector<Bullet>vector<Monster>vector<Loot>等。这并没有听起来那么疯狂,原因有三:

  1. 通常,您可以将管理这样一个矢量的整个业务拉到专用系统中。所以最后你甚至可能有一个vector<System *>,其中每个System都有一个向量用于一种东西,并在一个虚拟调用中更新所有这些东西(委托给许多静态分配的调用)。
  2. 您不需要在此抽象中表示中的所有。不是每个小整数都需要用自己的实体类型包装。
  3. 如果您沿着这条路线走下去,并从实体组件系统中获得提示,您还可以获得用于代码重用的继承的另一种选择(class Monster : Entity {}; class Skeleton : Monster {};),它具有来之不易的缓存友好性。

这并不容易,因为多态性不能很好地与缓存一致性一起工作。

我认为最好是重载基类的new操作符来从池中分配内存。但是要做到这一点,您需要知道所有派生类的大小,并且在进行一些分配/释放之后,您可以使用内存碎片,这将降低增益。

看看Cachegrind,它是一个模拟程序如何与机器的缓存层次结构交互的工具。

最新更新