迭代具有共同基类的对象在有条件的内存中

时间:2015-06-27 23:45:54

标签: c++ vector

我试图弄清楚如何迭代在内存中共享公共基本父类连续的容器(如std :: vector)。

为了演示此问题,请使用以下示例。

class Base
{
public:
    Base();
    virtual void doStuff() = 0;
};

class DerivedA : public Base
{
private:
    //specific A member variables
public:
    DerivedA();
    virtual void doStuff();
};

class DerivedB : public Base
{
private:
    //specific B member variables
public:
    DerivedB();
    virtual void doStuff();
};

现在,使用std :: vector进行迭代会将对象保留在连续的内存中,但我们会遇到切片,因为派生属性没有空间。

所以我们必须使用类似指针的多态技术

int main ()
{
    std::vector<Base*> container;
    container.push_back(new DerivedA());
    container.push_back(new DerivedB());

    for (std::vector<Base*>::iterator i = container.begin(); i!=container.end(); i++)
    {
        (*(*i)).doStuff();
    }
}

据我所知,鉴于这些类已经实现,应该可以正常工作。

问题: 现在,向量包含连续内存中的指针,但这并不意味着它们指向的地址是。

因此,如果我希望能够随时在对象中删除和插入对象,则对象将遍布内存中的所有位置。

问题: 似乎每个人都建议以std :: vector的方式进行操作,但为什么在内存中它不能连续迭代(假设我们实际使用指针)是不是有问题呢?

我是否被迫以复制面食的方式做到了?

int main ()
{

    std::vector<DerivedA> containerA;
    DerivedA a;
    containerA.push_back(a);

    std::vector<DerivedB> containerB;
    DerivedB b;
    containerB.push_back(b);

    for (std::vector<DerivedA>::iterator i = containerA.begin(); i!=container.end(); i++)
    {
        (*i).doStuff();
    }
    for (std::vector<DerivedB>::iterator i = containerB.begin(); i!=container.end(); i++)
    {
        (*i).doStuff();
    }
}

我猜测可能没有一个真正的解决方案,因为在内存中保持各种大小的线性对象并不是真的有意义但是如果有人能给我一些建议我会很感激

6 个答案:

答案 0 :(得分:3)

让我们按顺序回答问题。

问:如何创建一个连续的异构容器?

A:您不能。

假设您使用了一些 placement new 恶作剧将您的对象安排在内存中,如下所示:

  [B ][DA  ][DB      ][B ][B ][DB      ][DA  ]

迭代机制如何知道进行迭代的距离 从一个对象指向下一个对象的指针?从第一个开始的字节数 第二个元素与第二个元素不同。

连续数组必须均匀的原因是距离 从一个对象到下一个对象是所有元素的 same 。你可能 尝试在每个对象中嵌入一个大小,但是基本上您有一个链接 列表而不是数组(尽管数组很好 locality

这种推理导致使用一个指针数组的想法,关于 您提出了下一个问题:

问:为什么它不能连续迭代为什么没有问题

A:它没有您想象的那么慢。

您的关注点似乎在于以下指向 分散的内存位置。但是遵循这些指针的代价是 不太可能占主导地位。不要迷恋像这样的微观优化 内存布局,直到有确凿的证据需要它们为止。

问:我是否被迫采用复制粘贴方式?

A:不!

在这里,关注点似乎是可维护性而不是性能。 我认为可维护性更为重要,这是一件好事 早点考虑。

对于可维护性,您已经有一个好的解决方案:维护一个 Base*的向量。

如果您真的想使用多个向量,还有更好的方法 比复制和粘贴:使用模板功能,例如(未测试):

template <class T>
void doStuffToVector(std::vector<T> &vec)
{
  for (std::vector<T>::iterator i = vec.begin(); i!=vec.end(); ++i) {
    (*i).doStuff();
  }
}

然后在每个容器上调用它:

  doStuffToVector(containerA);
  doStuffToVector(containerB);

如果您只关心可维护性,则可以使用指针的向量 或模板函数就足够了。

问:有什么建议吗?

A:对于初学者而言,忽略性能,至少要保持不变 有关因素。专注于正确性和可维护性。

然后评估效果。观察到这个问题没有开始 带有当前和所需速度的说明。您还没有 实际要解决的问题!

测量后,如果得出结论太慢,请使用 profiler 找出慢点在哪里。他们几乎永远不在你身边 认为他们会的。

关键点:整个问题和答案都集中在 迭代,但是没有人提出虚拟函数 对doStuff的呼叫更有可能成为瓶颈!虚拟 函数调用很昂贵,因为它们是间接的 control 流, 造成重大问题 pipeline; 间接数据访问便宜得多,因为 data cache通常非常 有效地快速满足数据访问请求。

问:(暗示)我将如何优化它?

A:经过仔细测量,您可能会发现此代码 (迭代本身,包括虚拟函数分派;不是什么 doStuff内)是一个瓶颈。那一定意味着它正在执行 至少十亿个迭代。

首先,研究可以减少数量的算法改进 所需的迭代次数。

接下来,消除虚拟函数调用,例如通过嵌入一个 对象类型的显式指示符,并使用ifswitch对其进行测试。 这将允许处理器的 branch predictor至 更有效。

最后,是的,您可能希望将所有元素整合为一个 连续数组以提高局部性并消除间接数据 访问。那也将意味着消除类层次结构 对象是同一类型,可以将所有字段组合成一个 单班和/或使用union。这会损害您程序的 可维护性!有时这是高昂的写作成本之一 性能代码,但实际上很少需要。

答案 1 :(得分:0)

一个非常简单的解决方案是按地址值对指针数组进行排序。然后,如果您对向量进行迭代,它们将按照内存顺序排列。也许不是连续的,但顺序仍然如此,从而减少了缓存丢失。

真正拥有连续内存的唯一方法是这样分配内存,例如,将派生类型的对象向量存储在其自己的容器中,然后在指针向量中进行引用。

答案 2 :(得分:0)

  

似乎每个人都建议采用std::vector的方式,但是为什么   它不是在连续迭代的情况下被认为是有问题的   内存(假设我们实际上使用了指针)?

我不知道谁认为这有问题。与其他答案一样,在很多情况下,您根本不在乎。进行性能分析,您将看到是否需要对其进行优化。

在大多数情况下,人们会建议您使用std::vector<std::unique_ptr<...>>

尽管如此,在很多情况下,将对象存放在连续内存中非常重要。游戏就是其中一种情况。我写了很多计算代码(有限元库),在这里也很重要。您可以阅读有关如何以其他方式组织数据以使所有内容保持一致的信息。例如,将所有Arm对象存储在std::vector中而不是将每个Arm存储在Hero对象中并通过访问Arm对象可能会很有趣Hero对象。

无论如何,这是一种将示例中的对象存储在连续容器中的简便方法。

对于base class,使用alignas固定对象的大小。确保它足够大,以便所有派生对象都适合其中。在下面的示例中,DerivedA的大小为16,DerivedB的大小为24。指定的对齐大小必须为2的幂,因此我们选择32。

struct alignas(32) Base
{
    virtual void print() const {}
};

struct DerivedA : Base
{
    void print() const final override { std::cout << "num: " << i << std::endl; }
    int i = 1;
};

struct DerivedB : Base
{
    void print() const final override { std::cout << "num: " << i << std::endl; }
    int i = 2;
    double j = 100.0;
};

现在我们可以用DerivedA编写DerivedBplacement new的实例:

int main ()
{
    std::vector<Base> v(2);
    new (&v[0]) DerivedA();
    new (&v[1]) DerivedB();

    for (const auto& e : v)
        e.print();

    return 0;
}

编辑

这里的问题是您需要手动管理尺寸。另外,正如最近向我指出的那样,alignas旨在将对象定位在内存中,而不是分配大小。也许更好的方法是只使用std::variant

int main()
{
    std::vector<std::variant<DerivedA, DerivedB>> vec;
    vec.emplace_back(DerivedA());
    vec.emplace_back(DerivedB());
    for (const auto& e : vec)
        std::visit(VisitPackage(), e);
    return 0;
}

其中VisitPackage可能是这样的:

struct VisitPackage
{
    void operator()(const DerivedA& d) { d.print(); }
    void operator()(const DerivedB& d) { d.print(); }
};

下面是一个完整而简短的示例,说明如何使用std::variant获得所需的内容。

#include <iostream>
#include <vector>
#include <variant>

struct Base { virtual void print() const = 0; };
struct DerivedA : Base { void print() const final override { std::cout << "DerivedA\n"; } };
struct DerivedB : Base { void print() const final override { std::cout << "DerivedB\n"; } };

struct Print
{
    template <typename T>
    // note that the operator() calls print from DerivedA or DerivedB directly
    void operator()(const T& obj) const { obj.print(); }
};

int main ()
{    
    using var_t = std::variant<DerivedA, DerivedB>;
    std::vector<var_t> vec { DerivedA(), DerivedB() };
    for (auto& e : vec)
        std::visit(Print(), e);

    return 0;
}

答案 3 :(得分:-1)

如果必须将对象存储在数组中,则必须固定其类型。然后我们有这些变体:

  • 动态分配并存储指针-如果要求对象在内存中是连续的,请使用自定义分配器
  • 使用固定大小的多态类型,例如union,作为存储类型

对于第二个变体,代码可能是这样的:

#include <new>

struct A {
    A() {}
    virtual void f() {}
};
struct B : A {
    B() {}
    void f() override {}
};

union U {
    A a;
    B b;
    U() {}
};

int main() {
    U u[2];
        new (&u[0]) A;
        new (&u[1]) B;
    ((A*)&u[0])->f(); // A::f
    ((A*)&u[1])->f(); // B::f
}

答案 4 :(得分:-2)

std::vector<T>迭代器假设连续内存中的对象属于T类型,std::vector<T>::iterator::operator++认为sizeof T是不变的 - 也就是说,它不会参考大小数据的特定实例。

从本质上讲,您可以将vectorvector::iterator视为T* m_data指针上的薄外观,这样iterator++实际上只是一个基本的指针操作。 / p>

您可能需要使用自定义分配器和就地new来准备数据,并附带索引,链接等。或许可以考虑http://www.boost.org/doc/libs/1_58_0/doc/html/intrusive/slist.html

之类的内容

另见boost::stable_vector

答案 5 :(得分:-3)

std::vector在连续内存中分配对象,但是在向量中存储的对象指针则不是。这是您遍历vector的方式。以下代码用c ++ 14编写。此解决方案无法解决所描述的问题,因为对象指针存储在连续内存中,而不是实际对象中。

#include <iostream>
#include <memory>
#include <vector>
#include <algorithm>
using namespace std;

class Base
{
public:
    Base() {}
    virtual void doStuff() = 0;
};

class DerivedA : public Base
{
private:
    //specific A member variables
public:
    DerivedA() : Base() {}
    virtual void doStuff() {
        std::cout << "Derived Class A - Do Stuff" << std::endl;
    }
};

class DerivedB : public Base
{
private:
    //specific B member variables
public:
    DerivedB() : Base() {}
    virtual void doStuff() {
        std::cout << "Derived Class B - Do Stuff" << std::endl;
    }
};
int main() {
    // your code goes here
    std::vector<std::unique_ptr<Base> > container;
    container.push_back(std::make_unique<DerivedA>());
    container.push_back(std::make_unique<DerivedB>());

    std::for_each(container.begin(), container.end(),[](std::unique_ptr<Base> & b) {
        b->doStuff();
    });
    return 0;
}

现场演示here