对于vector <char> </char>,令人惊讶的自定义分配器

时间:2013-03-06 01:39:44

标签: c++ c++11 allocator

我想在下面使用带有自定义分配器的向量,其中construct()destroy()有一个空体:

struct MyAllocator : public std::allocator<char> {
    typedef allocator<char> Alloc;
    //void destroy(Alloc::pointer p) {} // pre-c+11
    //void construct(Alloc::pointer p, Alloc::const_reference val) {} // pre-c++11
    template< class U > void destroy(U* p) {}
    template< class U, class... Args > void construct(U* p, Args&&... args) {}
    template<typename U> struct rebind {typedef MyAllocator other;};
};

现在由于我在another question中指定的原因,vector必须在循环中多次调整大小。为了简化我的性能测试,我做了一个非常简单的循环,如下所示:

std::vector<char, MyAllocator> v;
v.reserve(1000000); // or more. Make sure there is always enough allocated memory
while (true) {
   v.resize(1000000);
   // sleep for 10 ms
   v.clear(); // or v.resize(0);
};

我注意到,尽管分配器具有空construct()destroy()成员函数,但是将CPU消耗的大小从30%增加到80%。因此,我本来期望对性能产生非常小的影响或没有影响(启用优化)。消费增量如何可能?第二个问题是:为什么在调整大小后读取内存时,我看到调整大小的内存中每个char的值为0(我会期望一些非零值,因为constuct()什么都不做)?

我的环境是g ++ 4.7.0,启用了-O3级优化。 PC Intel双核,4GB可用内存。显然,对construct的来电根本无法优化?

2 个答案:

答案 0 :(得分:2)

更新

这是一个完整的重写。原帖/我的答案中有一个错误,这使我对同一个分配器进行了两次基准测试。糟糕。

嗯,我可以看到性能上的巨大差异。我制作了以下试验台,它采取了一些预防措施,以确保关键的东西没有完全优化。然后我验证了(使用-O0 -fno-inline)分配器的constructdestruct调用被调用了预期的次数(是):

#include <vector>
#include <cstdlib>

template<typename T>
struct MyAllocator : public std::allocator<T> {
    typedef std::allocator<T> Alloc;
    //void destroy(Alloc::pointer p) {} // pre-c+11
    //void construct(Alloc::pointer p, Alloc::const_reference val) {} // pre-c++11
    template< class U > void destroy(U* p) {}
    template< class U, class... Args > void construct(U* p, Args&&... args) {}
    template<typename U> struct rebind {typedef MyAllocator other;};
};

int main()
{
    typedef char T;
#ifdef OWN_ALLOCATOR
    std::vector<T, MyAllocator<T> > v;
#else
    std::vector<T> v;
#endif
    volatile unsigned long long x = 0;
    v.reserve(1000000); // or more. Make sure there is always enough allocated memory
    for(auto i=0ul; i< 1<<18; i++) {
        v.resize(1000000);
        x += v[rand()%v.size()];//._x;
        v.clear(); // or v.resize(0);
    };
}

时间差标记为:

g++ -g -O3 -std=c++0x -I ~/custom/boost/ test.cpp -o test 

real    0m9.300s
user    0m9.289s
sys 0m0.000s

g++ -g -O3 -std=c++0x -DOWN_ALLOCATOR -I ~/custom/boost/ test.cpp -o test 

real    0m0.004s
user    0m0.000s
sys 0m0.000s

我只能假设您所看到的内容与char的标准库优化分配器操作有关(它是POD类型)。

使用

时,时间间隔会变得更远
struct NonTrivial
{
    NonTrivial() { _x = 42; }
    virtual ~NonTrivial() {}
    char _x;
};

typedef NonTrivial T;

在这种情况下,默认分配器需要超过2分钟(仍在运行)。 而'虚拟'MyAllocator花费约0.006秒。 (注意,这会调用未定义的行为,引用尚未正确初始化的元素。)

答案 1 :(得分:0)

(感谢GManNickG和Jonathan Wakely在下面进行更正)

在C ++ 11中,http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3346.pdf提出了标准后修正,resize()将使用自定义分配器构建添加的元素。

在早期版本中,resize()值会初始化添加的元素,这需要时间。

这些初始化步骤与内存分配无关,它是在分配后对内存所做的操作。价值初始化是不可避免的费用。

鉴于当前编译器中符合C ++ 11标准的状态,值得查看标题以查看正在使用的方法。

值初始化有时不必要且不方便,但也保护了很多程序免受意外错误的影响。例如,有人可能认为他们可以调整std::vector<std::string>的大小,使其具有100个“未初始化”字符串,然后在读取之前开始分配它们,但是赋值运算符的先决条件是正在构造的对象已被正确构造...否则它可能会找到垃圾指针并尝试delete[]它。只有仔细放置new - 每个元素才能安全地构建它们。 API设计在稳健性方面存在错误。