我正在用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>
等。这并没有听起来那么疯狂,原因有三:
- 通常,您可以将管理这样一个矢量的整个业务拉到专用系统中。所以最后你甚至可能有一个
vector<System *>
,其中每个System
都有一个向量用于一种东西,并在一个虚拟调用中更新所有这些东西(委托给许多静态分配的调用)。 - 您不需要在此抽象中表示中的所有。不是每个小整数都需要用自己的实体类型包装。
- 如果您沿着这条路线走下去,并从实体组件系统中获得提示,您还可以获得用于代码重用的继承的另一种选择(
class Monster : Entity {}; class Skeleton : Monster {};
),它具有来之不易的缓存友好性。
这并不容易,因为多态性不能很好地与缓存一致性一起工作。
我认为最好是重载基类的new操作符来从池中分配内存。但是要做到这一点,您需要知道所有派生类的大小,并且在进行一些分配/释放之后,您可以使用内存碎片,这将降低增益。
看看Cachegrind,它是一个模拟程序如何与机器的缓存层次结构交互的工具。