【问题标题】:Efficient container for different derived classes in C++C++中不同派生类的高效容器
【发布时间】:2017-07-07 21:02:40
【问题描述】:

在编写游戏时,我曾经将所有游戏对象存储在具有初始化和固定大小的 std::vector 中。最近我觉得需要在游戏对象类之间进行一些继承。

假设我有 40 个类从我的类 Enemy 派生。如果我想将这些类的对象/实例存储在向量中,我只能选择将它们存储为向量 Enemy* 对吗?所以唯一连续分配的是指针,对吧?所以当需要取消引用时,我仍然会有很多缓存未命中,对吧?

是否有任何“最佳实践”方式,将派生类存储在连续分配的内存中,以便循环通过它们花费最少的时间?

【问题讨论】:

  • 我认为他在谈论类实例。这对我来说似乎很清楚。
  • 我指的是那些类的对象。实例。在运行时创建并分配的东西。
  • 使用指向向量内基类的指针,或者如果它更合适,则使用某种智能指针。
  • 我认为将它们存储在向量中 正是您要寻找的。向量似乎是 c++ 用于迭代和存储的最佳实践工具。而且由于它们是指针,所以它与矢量 相同,所以也很好。
  • 有几种方法,但它们在技术上都相当复杂。我强烈建议您从vector<unique_ptr<Enemy>> 和基准开始。如果您确实需要走这条路,那么这种具有大量派生类的最简单/最可行的方法是使用自定义分配器来确保所有 Enemies 都在连续的内存块中构建。您仍然会使用 unique_ptr 的向量和自定义删除器,并使用其他函数而不是 make_unique

标签: c++ memory vector containers allocation


【解决方案1】:

Boost 刚刚为此接受了一个库:poly_collection。特别是,您正在寻找base_collection

在内部,它使用许多向量,每个(派生)类型一个,同时提供接近标准容器的接口。

其作者This article 提供了一些设计背景以及与其他解决方案的比较,例如unique_ptr 的向量。优点是双重的:首先,通过不使用指针和每个元素的动态内存分配,您可以获得更好的内存局部性,其次,将相同类型的元素组合在一起,you help branch prediction 和用于虚拟成员函数的指令缓存。

【讨论】:

  • 完全不清楚如果将相同类型的元素组合在一起会更优化。在许多情况下,在时间上紧密分配的对象之间存在很强的关系。在其他情况下,重要的是只将派生类型的所有对象存储在一起,而无需分组。然后对象可能永远存在,并且全部一起被释放 - 然后,竞技场会表现得更好。与大多数事情一样,您必须在性能很重要时进行分析,poly_collection 可能会执行最好的替代方案,或者不是。
【解决方案2】:

这个怎么样?

struct alignas(...) Base {};
struct Derived1 : Base {};
struct Derived2 : Base {};

int main()
{
    std::vector<Base> v(2);
    new (&v[0]) Derived1();
    new (&v[1]) Derived2();
    return 0;
}

放置新就可以了。它适用于多态性。对齐是为了确保向量包含相同大小的对象。将... 替换为一个数字(2 的幂),以使Derived1Derived2 的实例都适合向量。如果sizeof(Derived1) 返回 16,sizeof(Derived2) 返回 24,则需要 alignas(32)

编辑

正如@KubaOber 所说,alignas 并非旨在管理分配大小,而仅用于将对象定位在内存中。实现目标的更好方法是使用std::variant。像这样的:

int main()
{
    std::vector<std::variant<Derived1, Derived2>> v;
    v.emplace_back(Derived1());
    v.emplace_back(Derived2());

    for (const auto& e : v)
        std::visit(VisitPackage(), e);

    return 0;
}

VisitPackage 可能是这样的:

struct VisitPackage
{
    void operator()(const Derived1&) { std::cout << "Derived 1.\n"; }
    void operator()(const Derived2&) { std::cout << "Derived 2.\n"; }
};

【讨论】:

  • 这样做的唯一结果是您必须手动管理大小和对齐方式,并且您也可能过度对齐,因为 alignas 从来没有打算管理分配大小,只是定位内存中的对象。
  • 基本上,你所做的是一起被黑std::vector&lt;std::variant&lt;Base, Derived1, Derived2&gt;&gt; :)
  • 真的哈哈。但是 std::visit 需要涵盖所有情况。无论如何,我同意你对 alignas 的评论。
猜你喜欢
  • 2013-08-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-12-04
  • 2018-05-27
  • 2021-06-18
  • 1970-01-01
  • 2013-06-24
相关资源
最近更新 更多