Recently I put together a custom fixed size (bounded) memory pool for my job system implementation to support its internal memory management. Since it will be accessed by different threads I want to make sure that the allocation and deallocation itself is atomic. However, I 'm not sure how to test it to make sure it's actually working. Also, it must support memory alignemnts as well to avoid false acquisition, with also avoiding locks and minimize waits.
These were my main criteria, however, I'd like to go further, and support growth in pool size or support objects (call ctor and dtor), etc.
This was my first quick and dirty attempt to implement this, and meeting my criteria.
#include <cstddef>
#include <atomic>
#include <cstdint>
#include <cassert>
#include <cstdlib>
namespace JobSystem
{
class MemoryPoolAllocator
{
public:
MemoryPoolAllocator(size_t elementSize, size_t numElements, size_t alignment = 16);
MemoryPoolAllocator(const MemoryPoolAllocator & alloc) = delete;
MemoryPoolAllocator & operator=(const MemoryPoolAllocator & rhs) = delete;
MemoryPoolAllocator(MemoryPoolAllocator && alloc) = delete;
MemoryPoolAllocator & operator=(MemoryPoolAllocator && rhs) = delete;
~MemoryPoolAllocator();
void * Allocate() noexcept;
void Deallocate(void * pBlock) noexcept;
size_t ElementSize() const { return mElementSize; }
size_t Capacity() const { return mPoolSize; }
private:
bool AllocatePool(size_t elementSize, size_t numElements, size_t alignment);
void ReleasePool();
size_t mPoolSize = 0;
size_t mElementSize = 0;
size_t mAlignment = 0;
void * mMemory = nullptr;
std::atomic<void **> mHead;
};
} // namespace JobSystem
JobSystem::MemoryPoolAllocator::MemoryPoolAllocator(const std::size_t elementSize, const std::size_t numElements, std::size_t alignment)
{
mHead.store(nullptr);
const bool result = AllocatePool(elementSize, numElements, alignment);
assert(result);
}
JobSystem::MemoryPoolAllocator::~MemoryPoolAllocator() { ReleasePool(); }
bool JobSystem::MemoryPoolAllocator::AllocatePool(const std::size_t elementSize, const std::size_t numElements, const std::size_t alignment)
{
assert(mMemory == nullptr);
mElementSize = elementSize;
mAlignment = alignment;
assert(mElementSize >= sizeof(void *));
assert(mElementSize % mAlignment == 0);
assert((mAlignment & (mAlignment - 1)) == 0);
mPoolSize = (mElementSize * numElements) + alignment;
mMemory = aligned_alloc(mAlignment, mElementSize * numElements);
if (mMemory == nullptr) return false;
void ** freeMemoryList = static_cast<void **>(mMemory);
mHead.store(freeMemoryList);
const auto endAddress = reinterpret_cast<uintptr_t>(freeMemoryList) + (elementSize * numElements);
for (size_t element = 0; element < numElements; ++element) {
const auto currAddress = reinterpret_cast<uintptr_t>(freeMemoryList) + element * mElementSize;
const auto nextAddress = currAddress + mElementSize;
void ** currMemory = reinterpret_cast<void **>(currAddress);
if (nextAddress >= endAddress) { // last chunk
*currMemory = nullptr;
} else {
*currMemory = reinterpret_cast<void *>(nextAddress);
}
}
return true;
}
void * JobSystem::MemoryPoolAllocator::Allocate() noexcept
{
assert(mMemory);
void ** pHead = mHead.load(std::memory_order_relaxed);
if (pHead != nullptr) {
void * pBlock = reinterpret_cast<void *>(pHead);
void ** pNext = static_cast<void **>(*pHead);
mHead.compare_exchange_weak(pHead, pNext);
return pBlock;
}
return nullptr;
}
void JobSystem::MemoryPoolAllocator::Deallocate(void * pBlock) noexcept
{
if (pBlock == nullptr) { return; }
assert(mMemory);
void ** pHead = mHead.load(std::memory_order_relaxed);
if (pHead == nullptr) {
void ** pPrev = reinterpret_cast<void **>(pBlock);
*pPrev = nullptr;
mHead.compare_exchange_weak(pHead, pPrev);
} else {
void ** ppReturnedBlock = pHead;
void ** pPrev = reinterpret_cast<void **>(pBlock);
*pHead = reinterpret_cast<void *>(ppReturnedBlock);
mHead.compare_exchange_weak(pHead, pPrev);
}
}
void JobSystem::MemoryPoolAllocator::ReleasePool()
{
std::free(mMemory);
mMemory = mHead = nullptr;
}
Allocate
norDeallocate
are thread safe. Theassert
inDeallocate
seems to be wrong. One way you can test it is to create a program with several threads that are repeatedly allocating and freeing memory. \$\endgroup\$