【问题标题】:Custom STL Allocator: Construct/Destroy called for each element自定义 STL 分配器:为每个元素调用构造/销毁
【发布时间】:2022-11-29 20:26:44
【问题描述】:

我有这个代码 sn-p

auto start = high_resolution_clock::now();

std::vector<char> myBuffer(20e6);

std::cout << "StandardAlloc Time:" << duration_cast<milliseconds>(high_resolution_clock::now() - start).count() << std::endl;

start = high_resolution_clock::now();

std::vector<char, HeapAllocator<char>>myCustomBuffer(20e6);

std::cout << "CustomAlloc Time:" << duration_cast<milliseconds>(high_resolution_clock::now() - start).count() << " CC: " <<  HeapAllocator<char>::constructCount << std::endl;

输出:

StandardAlloc Time:6
CustomAlloc Time:124 CC: 20000000

有了这个分配器

template<class T>
struct HeapAllocator
{
    typedef T value_type;

    HeapAllocator(){};

    template<class U>
    constexpr HeapAllocator(const HeapAllocator<U>&) noexcept {}

    [[nodiscard]] T* allocate(std::size_t n)
    {
        auto p = new T[n];
        return p;
    }

    void deallocate(T* p, std::size_t n) noexcept
    {
        delete p;
    }

    template <class U>
    void destroy(U* p)
    {
        destroyCount++;
    }

    template< class U, class... Args >
    void construct(U* p, Args&&... args)
    {
        constructCount++;
    }
    static int destroyCount;
    static int constructCount;
};

template<class T>
int HeapAllocator<T>::constructCount = 0;

因此很明显,与默认分配器相比,缓冲区的每个 char 元素都会调用构造/销毁,这导致执行时间增加了 20 倍。我怎样才能防止这种基本类型的这种行为?

【问题讨论】:

  • 分配器不负责调用构造函数;那是std::vector 的工作。 new T[n] 确实调用了构造函数......例如std::vector&lt;MyType, MyAllocator&gt; v; v.reserve(100); v.emplace_back(param); 应该导致对非默认构造函数的恰好 1 次构造函数调用,但是对于您的分配器实现,这将导致(至少)对默认​​构造函数的 100 次调用。
  • 顺便说一句:您使用 delete 而不是 delete[] 会导致未定义的行为。你不应该调用任何 delete 运算符,因为这将涉及调用析构函数,这也是 std::vector...

标签: c++ allocator


【解决方案1】:

您根本不声明它们。它们将通过std::allocator_traits默认使用std::construct_at/std::destroy_at(即放置新和(伪)析构函数调用),就像std::allocator一样。这适用于 C++11。

这些函数只需要那么多时间,因为您正在修改它们中的静态。此外,您的实施没有执行他们需要做的工作。 construct,如果你声明它,必须通过一种或另一种方式在内存中构造一个T类型的对象。并且 destroy 必须调用析构函数,至少对于没有平凡析构函数的类型。

也就是说,std::vector&lt;char&gt; myBuffer(20e6);std::vector&lt;char, HeapAllocator&lt;char&gt;&gt;myCustomBuffer(20e6); 仍然需要一些时间,因为它们将分配的内存归零。 std::vector 不提供任何接口使其处于未初始化状态,并且在一定程度上依赖于编译器识别 std::construct_at 循环可以优化为 memset。 (对于std::allocator,标准库在看到使用std::allocator时可以直接专门化为memset。)

但是,std::vector 提供了 .reserve,它将通过 allocate 保留足够的内存,而无需在其中构造任何对象。然后可以根据需要使用push_back/emplace_back/resize创建对象。


您的分配器还有其他一些问题:

  • auto p = new T[n]; 错了。 allocate 应该分配内存,而不是构造对象。您应该只分配适当大小和对齐的内存,例如::operator new[](n*sizeof(T), std::align_val_t{alignof(T)}),在对乘法进行溢出检查之后。类似地,deallocate 应该使用匹配的释放函数(例如::operator delete[])。除此之外 deletenew[] 也与未定义的行为不匹配。 new[]你之后必须使用delete[],而不是delete

  • 您的课程缺少 operator==(以及 C++20 之前的 operator!=)。它们是必需的,如果您的类没有任何非静态数据成员并且没有定义 is_always_equal 成员,则 operator== 必须始终返回 trueoperator!= 始终返回 false

【讨论】:

    猜你喜欢
    • 2011-01-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-09-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-04-29
    相关资源
    最近更新 更多