HiveBrain v1.2.0
Get Started
← Back to all entries
patterncppMinor

Auto-recycling C++11 polymorphic smart pointers

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
polymorphicautorecyclingsmartpointers

Problem

I've recently read an interesting blog post by Philipp Zschoche:
it explains how it's possible to avoid unnecessary allocations/deallocations by keeping track of previously allocated memory in a stack.

It's very easy to code a drop-in replacement for smart pointers (such as std::unique_ptr and std::shared_ptr) that will make use of the memory recycler.

When creating a smart pointer, before allocating, it's sufficient to check if there is any unused allocated memory block in the recycler: if there is one, use it, otherwise allocate new memory.

Upon destroying a smart pointer, instead of deallocating, simply call the object's destructor and push the now undefined/unusable memory block on the memory recycler stack, so that other smart pointers of the same type can make use of it.

The memory will be only deallocated at the end of the program (or manually).

```
namespace ssvu
{
namespace Internal
{
template class Recycler
{
private:
std::vector ptrs;
std::allocator alloc;

inline T* pop() noexcept
{
SSVU_ASSERT(!ptrs.empty());

auto result(ptrs.back());
ptrs.pop_back();
return reinterpret_cast(result);
}

public:
inline Recycler() = default;

// Deallocate all memory on destruction
inline ~Recycler() noexcept { for(auto p : ptrs) alloc.deallocate(reinterpret_cast(p), 1); }

// Recycle: call the allocated object's destructor, but do not deallocate memory
// Instead, store the pointer to the allocated memory block for reuse
inline void recycle(TBase* mPtr) noexcept(noexcept(alloc.destroy(mPtr)))
{
SSVU_ASSERT(mPtr != nullptr);
alloc.destroy(mPtr);
ptrs.emplace_back(mPtr);
}

Solution

My main complaint is the use of the std::vector in such a low level interface. I think that is totally a waste of resources.

Would be interested to know how this performs in comparison to you vector based version.

I basically swapped the std::vector for a TBase* and built a chain. It makes the assumption that TBase is at least the size of a pointer (but I am sure we can add a static assert to achieve this).

namespace ssvu
{
    namespace Internal
    {
        template class Recycler
        {
            struct Chain
            {
                Chain*  next;
            };
            private:
                Chain*            chain;
                std::allocator alloc;

                T* pop() noexcept
                {
                    SSVU_ASSERT(chain != nullptr);

                    TBase*  result  = reinterpret_cast(chain);
                    chain           = chain->next;

                    return result;
                }

            public:
                Recycler()
                    : chain(nullptr)
                {}

                // Deallocate all memory on destruction
                ~Recycler() noexcept
                {
                    Chain*  next;
                    for(;chain != nullptr; chain = next)
                    {
                        next    = chain->next;
                        alloc.deallocate(reinterpret_cast(chain), 1);
                    }
                }

                // Recycle: call the allocated object's destructor, but do not deallocate memory
                // Instead, store the pointer to the allocated memory block for reuse
                void recycle(TBase* mPtr) noexcept(noexcept(alloc.destroy(mPtr)))
                {
                    SSVU_ASSERT(mPtr != nullptr);
                    alloc.destroy(mPtr);

                    Chain*   newHead = reinterpret_cast(mptr);
                    newHead->next   = chain;
                    chain           = newHead;
                }

                // Create: either reuse allocated unused memory, or allocate a new memory block
                template T* create(TArgs&&... mArgs)
                {
                    auto result(chain == nullptr ? alloc.allocate(1) : pop());
                    alloc.construct(result, std::forward(mArgs)...);
                    return result;
                }
        };
        template Recycler& getRecycler() noexcept
        {
            static Recycler result; return result;
        }
    }

    // Uptr is a typedef for std::unique_ptr

    template    using UptrRecPoly   = Uptr;
    template                    using UptrRec       = UptrRecPoly;
    template                using VecUptrRec    = std::vector>;

    template inline UptrRecPoly makeUptrRecPoly(TArgs&&... mArgs)
    {
        return {Internal::getRecycler().create(std::forward(mArgs)...), [](TBase* mPtr)
        {
            Internal::getRecycler().recycle(mPtr);
        }};
    }
    template inline UptrRec makeUptrRec(TArgs&&... mArgs) { return makeUptrRecPoly(std::forward(mArgs)...); }
}


Other things I did not like

(but are totally my own personal preference so not that important).

Lining up stuff so it is easy to read and maintain:

template    using UptrRecPoly   = Uptr;
    template                    using UptrRec       = UptrRecPoly;
    template                using VecUptrRec    = std::vector>;


I believe that makes it a lot easier to read.

Don't put inline where it is not needed.

The keyword inline plays absolutely no role in code inlining. Don't use it for that. It only plays a role in the one definition rule so you need it if a function is defined in a header file that is included into multiple compilation units.

Code Snippets

namespace ssvu
{
    namespace Internal
    {
        template<typename T, typename TBase> class Recycler
        {
            struct Chain
            {
                Chain*  next;
            };
            private:
                Chain*            chain;
                std::allocator<T> alloc;

                T* pop() noexcept
                {
                    SSVU_ASSERT(chain != nullptr);

                    TBase*  result  = reinterpret_cast<TBase*>(chain);
                    chain           = chain->next;

                    return result;
                }

            public:
                Recycler()
                    : chain(nullptr)
                {}

                // Deallocate all memory on destruction
                ~Recycler() noexcept
                {
                    Chain*  next;
                    for(;chain != nullptr; chain = next)
                    {
                        next    = chain->next;
                        alloc.deallocate(reinterpret_cast<TBase*>(chain), 1);
                    }
                }

                // Recycle: call the allocated object's destructor, but do not deallocate memory
                // Instead, store the pointer to the allocated memory block for reuse
                void recycle(TBase* mPtr) noexcept(noexcept(alloc.destroy(mPtr)))
                {
                    SSVU_ASSERT(mPtr != nullptr);
                    alloc.destroy(mPtr);

                    Chain*   newHead = reinterpret_cast<Chain*>(mptr);
                    newHead->next   = chain;
                    chain           = newHead;
                }

                // Create: either reuse allocated unused memory, or allocate a new memory block
                template<typename... TArgs> T* create(TArgs&&... mArgs)
                {
                    auto result(chain == nullptr ? alloc.allocate(1) : pop());
                    alloc.construct(result, std::forward<TArgs>(mArgs)...);
                    return result;
                }
        };
        template<typename T, typename TBase> Recycler<T, TBase>& getRecycler() noexcept
        {
            static Recycler<T, TBase> result; return result;
        }
    }

    // Uptr<T, TDeleter> is a typedef for std::unique_ptr<T, TDeleter>

    template<typename T, typename TBase>    using UptrRecPoly   = Uptr<T, void(*)(TBase*)>;
    template<typename T>                    using UptrRec       = UptrRecPoly<T, T>;
    template<typename TBase>                using VecUptrRec    = std::vector<UptrRec<TBase>>;

    template<typename T, typename TBase, typename... TArgs> inline UptrRecPoly<T, TBase> makeUptrRecPoly(TArgs&&... mArgs)
    {
        return {Internal::getRecycler<T, TBase>().create(std::forward<TArgs>(mArgs)...), [](TBase* mPtr)
        {
            Internal::getRecycler<T, TBase>().recycle(mPtr);
        }};
    }
    template<typename T, typename... TArgs> inline UptrRec<T> makeUptrRec(TArgs&&... mArgs) { return makeUptrRecPol
template<typename T, typename TBase>    using UptrRecPoly   = Uptr<T, void(*)(TBase*)>;
    template<typename T>                    using UptrRec       = UptrRecPoly<T, T>;
    template<typename TBase>                using VecUptrRec    = std::vector<UptrRec<TBase>>;

Context

StackExchange Code Review Q#52433, answer score: 2

Revisions (0)

No revisions yet.