简体   繁体   中英

Custom STL Allocator: Construct/Destroy called for each element

I have this code snippet

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;

Output:

StandardAlloc Time:6
CustomAlloc Time:124 CC: 20000000

With this allocator

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;

So it's obvious that construct/destroy are called for each char element of the buffer which results in 20x in execution time compared to default allocator. How can I prevent this behavior for this basic types?

You simply do not declare them at all. They will be defaulted through std::allocator_traits to use std::construct_at / std::destroy_at (ie a placement-new and (pseudo-)destructor call) instead, just as for std::allocator . This applies since C++11.

The functions are only taking so much time because you are modifying a static in them. Also, your implementations are not performing the job they need to do. construct , if you declare it, must construct an object of type T in the memory via a placement-new one way or another. And destroy must call the destructor, at least for types without trivial destructor.

That said, std::vector<char> myBuffer(20e6); and std::vector<char, HeapAllocator<char>>myCustomBuffer(20e6); will still take some time since they zero the allocated memory. std::vector doesn't offer any interface to leave it uninitialized and there is some dependence on the compiler recognizing that the std::construct_at loop can be optimized to a memset . (For std::allocator the standard library can simply specialize to memset directly when it sees std::allocator is used.)

However, std::vector offers .reserve instead which will reserve enough memory through allocate without constructing any objects in it. Objects can then be created with push_back / emplace_back / resize as required later.


Also your allocator has some other issues:

  • auto p = new T[n]; is wrong. allocate is supposed to allocate memory, not to construct objects. You should be only allocating memory of appropriate size and alignment, eg with ::operator new[](n*sizeof(T), std::align_val_t{alignof(T)}) , after an overflow check on the multiplication. Similarly deallocate should then use the matching deallocation function (eg ::operator delete[] ). That aside delete and new[] are a mismatch with undefined behavior as well. After new[] you must use delete[] , not delete .

  • Your class lacks operator== (and operator!= before C++20). They are required and if your class doesn't have any non-static data members and doesn't define a is_always_equal member, then operator== must always return true and operator!= always false .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM