Custom C++ allocator for alligned data

Started by
21 comments, last by Zipster 6 years, 6 months ago

All very interesting code snippets about memory allocation, but I was actually only concerned with the AlignedAllocator instead of the actual allocation ;)

🧙

Advertisement

Are you aware of placement new and placement delete? These can be used to invoke constructor/desctructor code, while the use of malloc/free (or similar functions) are just for the allocations.

Do you need a custom C++ allocator for this? This is just so you can customise the allocation logic of std::vector, etc, right? Does the default one not do the right thing of calling your custom new operator?

12 hours ago, Randy Gaul said:

Are you aware of placement new and placement delete? These can be used to invoke constructor/desctructor code, while the use of malloc/free (or similar functions) are just for the allocations.

I figured this out in the meantime:


new ((void *)data) DataU(std::forward< ConstructorArgsT >(args)...);

No allocation will happen. The allocation/deallocation vs construction/destruction difference is now clear. For clarity, my final code is:


inline void *AllocAligned(size_t size, size_t alignment = 16) noexcept {
    return _aligned_malloc(size, alignment);
}

template < typename DataT >
inline DataT *AllocAlignedData(size_t count, size_t alignment = 16) noexcept {
    return static_cast< DataT * >(AllocAligned(count * sizeof(DataT), alignment));
}

inline void FreeAligned(void *ptr) noexcept {
    if (!ptr) {
        return;
    }

    _aligned_free(ptr);
}

template< typename DataT >
struct AlignedData {

    static void *operator new(size_t size) {
        const size_t alignment = __alignof(DataT);
        
        void * const ptr = AllocAligned(size, alignment);
        if (!ptr) {
            throw std::bad_alloc();
        }

        return ptr;
    }

    static void operator delete(void *ptr) noexcept {
        FreeAligned(ptr);
    }

    static void *operator new[](size_t size) {
        return operator new(size);
    }

    static void operator delete[](void *ptr) noexcept {
        operator delete(ptr);
    }
};

template< typename DataT, size_t AlignmentS = __alignof(DataT) >
struct AlignedAllocator final {
    
    using value_type = DataT;
    using pointer =  DataT *;
    using reference = DataT &;
    using const_pointer = const DataT *;
    using const_reference = const DataT &;
    using size_type = size_t;
    using difference_type = ptrdiff_t;

    template< typename DataU >
    struct rebind final {

        using other = AlignedAllocator< DataU, AlignmentS >;
    };

    AlignedAllocator() noexcept = default;
    AlignedAllocator(
        const AlignedAllocator< DataT, AlignmentS > &allocator) noexcept = default;
    AlignedAllocator(
        AlignedAllocator< DataT, AlignmentS > &&allocator) noexcept = default;
    template< typename DataU >
    AlignedAllocator(
        const AlignedAllocator< DataU, AlignmentS > &allocator) noexcept {}
    ~AlignedAllocator() = default;
    AlignedAllocator< DataT, AlignmentS > &operator=(
        const AlignedAllocator< DataT, AlignmentS > &allocator) noexcept = delete;
    AlignedAllocator< DataT, AlignmentS > &operator=(
        AlignedAllocator< DataT, AlignmentS > &&allocator) noexcept = delete;

    
    DataT *address(DataT &data) const noexcept {
        return &data;
    }
    
    const DataT *address(const DataT &data) const noexcept {
        return &data;
    }

    size_t max_size() const noexcept {
        return (static_cast< size_t >(0) - static_cast< size_t >(1)) 
            / sizeof(DataT);
    }

    DataT *allocate(size_t count) const {
        DataT * const data = AllocAlignedData(count, AlignmentS);
        if (!data) {
            throw std::bad_alloc();
        }

        return data;
    }

    template< typename DataU >
    DataT *allocate(size_t count, const DataU *hint) const {
        (void)hint;
        return allocate(count);
    }

    void deallocate(DataT *data, size_t count) const {
        (void)count;
        FreeAligned((void *)data);
    }
    
    template< typename DataU, typename... ConstructorArgsT >
    void construct(DataU *data, ConstructorArgsT&&... args) const {
        new ((void *)data) DataU(std::forward< ConstructorArgsT >(args)...);
    }

    template< typename DataU >
    void destroy(DataU *data) const {
        data->~DataU();
    }

    bool operator==(
        const AlignedAllocator< DataT, AlignmentS > &rhs) const noexcept {
        
        return true;
    }

    bool operator!=(
        const AlignedAllocator< DataT, AlignmentS > &rhs) const noexcept {
        
        return false;
    }
};

 

🧙

9 hours ago, Hodgman said:

Do you need a custom C++ allocator for this? This is just so you can customise the allocation logic of std::vector, etc, right? Does the default one not do the right thing of calling your custom new operator?

This is for using smart pointers. std::make_shared (for constructing std::shared_ptr) will allocate the control and data block together with one allocation, ignoring custom operator new. An alternative is defining your own std::make_shared in a fashion similar to std::make_unique which explicitly invokes operator new, but it seems cleaner to be a bit more conform to the standard and just define an allocator.

Furthermore, I removed the implicit dependence on AlignedData, so if you want to align your objects, irrespective of the need to align them, you can do so with the custom allocator. Idd. for primitive types (e.g. __m128). 

🧙

On 10/4/2017 at 1:52 AM, Hodgman said:

Do you need a custom C++ allocator for this? This is just so you can customise the allocation logic of std::vector, etc, right? Does the default one not do the right thing of calling your custom new operator?

Standard new (and therefore the default allocator as well), don't align by more than the maximum alignment of any built in types, and __m128 for example is not a C++ builtin type

8 minutes ago, l0calh05t said:

Standard new (and therefore the default allocator as well), don't align by more than the maximum alignment of any built in types, and __m128 for example is not a C++ builtin type

He's got a custom new that calls _aligned_malloc, I assumed the default allocator would be nice enough to use the type's custom new :(

This is one of those dark corners of C++ for me because every game project I've worked on has just straight up banned the new keyword (besides placement new inside their own allocation header) :D 

3 minutes ago, Hodgman said:

He's got a custom new that calls _aligned_malloc, I assumed the default allocator would be nice enough to use the type's custom new :(

This is one of those dark corners of C++ for me because every game project I've worked on has just straight up banned the new keyword (besides placement new inside their own allocation header) :D 

Allocators are very ugly.

For an aligned allocator, you could always use the one included with Eigen https://eigen.tuxfamily.org/dox/classEigen_1_1aligned__allocator.html

2 hours ago, Hodgman said:

He's got a custom new that calls _aligned_malloc, I assumed the default allocator would be nice enough to use the type's custom new :(

This is one of those dark corners of C++ for me because every game project I've worked on has just straight up banned the new keyword (besides placement new inside their own allocation header) :D 

Also, due to the separation of allocate/deallocate and construct/destroy, the C++ allocators cannot directly use new/delete (makes sense if you consider that std::vector can allocate memory on reserve without constructing anything, which isn't possible with custom new/delete)

2 hours ago, Hodgman said:

He's got a custom new that calls _aligned_malloc, I assumed the default allocator would be nice enough to use the type's custom new :(

Don't really like it myself but yeah that's the standard.

13 minutes ago, l0calh05t said:

Also, due to the separation of allocate/deallocate and construct/destroy, the C++ allocators cannot directly use new/delete (makes sense if you consider that std::vector can allocate memory on reserve without constructing anything, which isn't possible with custom new/delete)

Idd. that is basically the reason behind the "count" parameter, I assume, you can directly allocate a larger memory block, just in case.

🧙

This topic is closed to new replies.

Advertisement