我正在研究一些来自本文介绍了游戏编程中的ECS-系统.的代码,并试图理解它,我经常看到的是在那些似乎没有好处的地方使用堆内存。以这为例:
class ECS
{
public:
void someFunction()
{
archetypes.push_back(new Archetype);
}
~ECS()
{
for(Archetype* a : archetypes_)
{
delete a;
}
}
private:
std::vector<Archetype*> archetypes_;
};这是代码中在内存中操作原型的唯一方法。其余的代码只是使用它们。
为什么您会选择为此使用分配的内存?我经常在代码中看到这种情况,在我看来,使用堆内存仅仅是因为它感觉是正确的,而不是真正考虑它是否合适。std::vector已经在幕后使用堆内存了,那么当我们想要添加一个新的原型并让向量处理分配时,为什么不直接将一个堆栈变量复制到向量中呢?
class ECS
{
public:
void someFunction()
{
archetypes.push_back(Archetype());
}
private:
std::vector<Archetype> archetypes_;
};或者在这种情况下使用堆内存是否有正当的理由?
发布于 2021-11-07 10:30:20
这样做可能有几个原因,特别是:
使用new和delete会带来额外的负担和风险。自C++11以来,通常倾向于使用更安全的unique_ptr或shared_ptr:它们避免了内存管理的麻烦。
阅读完整篇文章并与提交人联系后,这里的手动内存管理似乎是出于性能原因:代码根据特定的内存布局组织对象。此外,在针对不同的游戏机时使用的一些工具链并不总是能很好地优化智能指针。因此,游戏行业将青睐原始指针。因此,手动微调内存管理。(我感谢作者在阅读这一答案时所表现出的反应能力和清晰性)
发布于 2021-11-11 13:07:05
我从ECS的角度来看,最常见的原因是避免在随后插入向量序列时使指针无效。组件通常希望彼此指向(它们可以通过索引而不是指针来实现,尽管在代码中不太方便)。例如,做FK/IK所需的运动育儿将需要组件存储到子组件的某种链接(指针或索引),以便将它们链接到一起(假设您没有使用完全独立的树结构存在于ECS之外)。您可以使用一个使用内存池的高效分配器来弥补operator new/delete的堆分配开销,比如具有固定时间分配/释放位置的自由职业者。
就我个人而言,我的性能要求非常严格,所以我不会这么做。我们的ECS通常有数百万个实体,在这里使用32位索引而不是64位架构上的64位指针,避免额外的堆分配(甚至使用内存池)。我们在服务器端使用ECS来实现具有巨大世界的MMO。但是如果它是10k实体而不是数百万,我认为在这里使用一系列指针,动态分配,比存储向量相对索引的整数更有效率。我们的编程负担是必须访问ECS场景以及索引,以便从引用它的源组件(而不是直接指针访问)访问特定组件,但我们发现,这是一种生产力妥协,在测量前后是值得的。
但是,我看到这样做的首要原因是避免使指针无效,就像std::vector<T>在插入时所做的那样,如果T不是指针的话。我不认为多态通常适用于需要在ECS中存储基本指针的情况,比如组件继承并重写虚拟函数的基本Component类型,因为ECS通常直接存储和获取特定类型的组件类型。如果这里使用多态性使代码更加类型安全,那么它通常应用于组件容器级别(即ComponentList基类型),而不是每个组件中的每个组件--至少稍微考虑到效率。如果ECS的设计使其能够将容器的内容转移到更高效的组件查询中,我可以将移动指针的能力看作是一个有效的理由,尽管在原型系统中,它对我来说没有什么意义,因为它根本上想避免在内存中使用可变跨步遍历多个类型的组件时效率低下的问题(即使预先按多组件类型顺序访问模式的地址对指针进行排序,也会导致许多缓存丢失)。他们通常希望尽量减少使用多类型组件查询的基本缓存丢失,因此他们通常在切入点级别重新排列和缓存内存,而不仅仅是指针/索引级别来优化查询。
因此,我认为这里最常见的原因是避免插入指针失效,这最终会给程序员带来方便。至少在实现了多个ECS体系结构之后,看看外面的一堆,这将是我认为这样做是一种以效率为代价的生产力妥协的首要原因(这在某些游戏中可能不是很大的代价,比如在特定场景中平均只有10k个实体.尽管在这些代码中实现原型可能不值得费心)。
https://softwareengineering.stackexchange.com/questions/433355
复制相似问题