Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 73 additions & 22 deletions include/pool_allocator/pool_allocator.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,19 @@
#include <stack>
#include <vector>

template <typename T, size_t BlockSize>
struct ExportedAlloc
{
using pointer = T*;
using size_type = std::size_t;

// Free slots in the block
std::stack<pointer, std::vector<pointer>> free_slots;

// Memory blocks - Optional, only used in _export_all and _import
std::vector<pointer> memory_blocks;
};

template <typename T, size_t BlockSize = 4096>
class PoolAllocator
{
Expand All @@ -55,35 +68,35 @@ class PoolAllocator
using size_type = std::size_t;
using difference_type = std::ptrdiff_t;
using propagate_on_container_copy_assignment = std::false_type;
using propagate_on_container_move_assignment = std::true_type;
using propagate_on_container_move_assignment = std::false_type;
using propagate_on_container_swap = std::true_type;
using is_always_equal = std::true_type;
using is_always_equal = std::false_type;

/* Legacy Rebind struct */
template <typename U>
struct rebind
{
typedef PoolAllocator<U, BlockSize> other;
};
// /* Legacy Rebind struct */
// template <typename U>
// struct rebind
// {
// typedef PoolAllocator<U, BlockSize> other;
// };

/* Member functions */
// Default constructor
PoolAllocator() noexcept;
// Copy constructor
PoolAllocator(const PoolAllocator& other) noexcept;
// Move constructor
PoolAllocator(PoolAllocator&& other) noexcept;
// Templated copy
// No Copy constructor
PoolAllocator(const PoolAllocator& other) = delete;
// No Move constructor
PoolAllocator(PoolAllocator&& other) = delete;
// No Templated copy
template <class U>
PoolAllocator(const PoolAllocator<U, BlockSize>& other) noexcept;
PoolAllocator(const PoolAllocator<U, BlockSize>& other) = delete;
// Destructor
~PoolAllocator() noexcept;

// Assignment operator
// We do not allow copy assignment for allocators
PoolAllocator& operator=(const PoolAllocator& other) = delete;
// Move assignment operator
PoolAllocator& operator=(PoolAllocator&& other) noexcept;
// We do not allow move assignment for allocators
PoolAllocator& operator=(PoolAllocator&& other) = delete;

// Address functions
pointer addressof(reference x) const noexcept;
Expand Down Expand Up @@ -126,30 +139,68 @@ class PoolAllocator
// Delete an object
void delete_object(pointer p);

// Debug helper functions
// Get total allocated size
inline size_type total_allocated_size() const noexcept
{
return memory_blocks.size() * BlockSize;
}

// Get total number of free slots
// Does not account for partial blocks
inline size_type total_free_slots() const noexcept
{
return free_slots.size();
}

// Transfer free slots from another allocator
void transfer_free(PoolAllocator<T, BlockSize>& from);
// Transfer all memory blocks and free slots from another allocator
void transfer_all(PoolAllocator<T, BlockSize>& from);

private:
// Allocate a memory block
void allocateBlock();

// Allocator import/export functions
// Export
//! Export only the available slots as a vector of pointers.
//! Warning: This does NOT transfer ownership of the underlying memory blocks.
//! Do NOT use this function in threads with shorter lifetimes than other threads
//! accessing objects backed by this allocator. Doing so may lead to use-after-free.
Comment on lines +167 to +170
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good warning. It may be useful to add to README.md some text that details the intended use of these functions.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Let me change that in the README.md docs.

ExportedAlloc<T, BlockSize> _export_free();

//! Export all the memory blocks + available slots
ExportedAlloc<T, BlockSize> _export_all();

// Import
//! Import all memory blocks and free slots from an ExportedAlloc
void _import(ExportedAlloc<T, BlockSize>& exported);

// Pointer to blocks of memory
std::vector<pointer> memory_blocks;
size_type current_block_slot = 0; // Current slot in the current block

// Free list
std::stack<pointer, std::vector<pointer>> free_slots;

// Number of items in one block (will be set by the constructor only)
size_type num_items;
size_type item_size;
};

// Operators
// Operator != and ==
template <typename T1, size_t B1, typename T2, size_t B2>
// Only two references to the same allocator are equal
template <typename T, size_t BlockSize>
inline bool
operator==(const PoolAllocator<T1, B1>&, const PoolAllocator<T2, B2>&) noexcept
operator==(const PoolAllocator<T, BlockSize>& a, const PoolAllocator<T, BlockSize>& b) noexcept
{
return B1 == B2;
return &a == &b;
}

template <typename T1, size_t B1, typename T2, size_t B2>
template <typename T, size_t BlockSize>
inline bool
operator!=(const PoolAllocator<T1, B1>& a, const PoolAllocator<T2, B2>& b) noexcept
operator!=(const PoolAllocator<T, BlockSize>& a, const PoolAllocator<T, BlockSize>& b) noexcept
{
return !(a == b);
}
Expand Down
135 changes: 88 additions & 47 deletions include/pool_allocator/pool_allocator.tcc
Original file line number Diff line number Diff line change
Expand Up @@ -38,91 +38,132 @@
template <typename T, size_t BlockSize>
PoolAllocator<T, BlockSize>::PoolAllocator() noexcept
{
// Calculate item alignment
constexpr size_type items_per_block = BlockSize / sizeof(T);
static_assert(items_per_block > 0, "Block size is too small for the type T");
this->num_items = items_per_block;
this->item_size = sizeof(T);
}

// Copy constructor
// Destructor
template <typename T, size_t BlockSize>
PoolAllocator<T, BlockSize>::PoolAllocator(const PoolAllocator& other) noexcept
PoolAllocator<T, BlockSize>::~PoolAllocator() noexcept
{
// Nothing should be done here
// Free all memory blocks
for (pointer block : memory_blocks)
{
::operator delete(block, std::align_val_t(alignof(T)));
}
}

// Move constructor
// Address functions
template <typename T, size_t BlockSize>
PoolAllocator<T, BlockSize>::PoolAllocator(PoolAllocator&& other) noexcept
typename PoolAllocator<T, BlockSize>::pointer
PoolAllocator<T, BlockSize>::addressof(reference x) const noexcept
{
// Move the memory blocks and free slots from the other allocator
memory_blocks = std::move(other.memory_blocks);
free_slots = std::move(other.free_slots);
current_block_slot = other.current_block_slot;
return std::addressof(x);
}

// Clear other allocator's states
other.current_block_slot = 0;
template <typename T, size_t BlockSize>
typename PoolAllocator<T, BlockSize>::const_pointer
PoolAllocator<T, BlockSize>::addressof(const_reference x) const noexcept
{
return std::addressof(x);
}

// Templated copy
template <typename T, size_t BlockSize>
template <class U>
PoolAllocator<T, BlockSize>::PoolAllocator(const PoolAllocator<U, BlockSize>& other) noexcept
inline ExportedAlloc<T, BlockSize>
PoolAllocator<T, BlockSize>::_export_free()
{
// Nothing should be done here
ExportedAlloc<T, BlockSize> exported;
exported.free_slots = std::move(free_slots);
// Clear the free slots stack
free_slots = std::stack<pointer, std::vector<pointer>>();

// No memory blocks to export
exported.memory_blocks = std::vector<pointer>();

return exported;
}

// Destructor
template <typename T, size_t BlockSize>
PoolAllocator<T, BlockSize>::~PoolAllocator() noexcept
ExportedAlloc<T, BlockSize>
PoolAllocator<T, BlockSize>::_export_all()
{
// Free all memory blocks
for (pointer block : memory_blocks)
ExportedAlloc<T, BlockSize> exported;
// Before moving the free slots, unwind the partially free bump-allocation block
// Add its free slots to the exported free slots
if (!memory_blocks.empty() && current_block_slot < num_items)
{
::operator delete(block, std::align_val_t(alignof(T)));
// Convert the partially free (bump allocated) blocks to free slots
for (size_type i = current_block_slot; i < num_items; ++i)
{
// Push the pointer to the free slots stack
exported.free_slots.push(memory_blocks.back() + i);
}
}
// Append existing free slots to the unwinded free slots
exported.free_slots.c.insert(exported.free_slots.c.end(), free_slots.c.begin(),
free_slots.c.end());
// Clear the free slots stack
free_slots = std::stack<pointer, std::vector<pointer>>();

// Move memory blocks to the exported struct
exported.memory_blocks = std::move(memory_blocks);
// Clear the memory blocks (don't free them)
memory_blocks = std::vector<pointer>();

// Reset the current block slot in the allocator
current_block_slot = 0;

return exported;
}

// Move assignment operator
template <typename T, size_t BlockSize>
PoolAllocator<T, BlockSize>&
PoolAllocator<T, BlockSize>::operator=(PoolAllocator&& other) noexcept
void
PoolAllocator<T, BlockSize>::_import(ExportedAlloc<T, BlockSize>& exported)
{
// Append the free slots from the exported allocator
free_slots.c.insert(free_slots.c.end(), exported.free_slots.begin(), exported.free_slots.end());

// Move the memory blocks and free slots from the other allocator
memory_blocks = std::move(other.memory_blocks);
free_slots = std::move(other.free_slots);
current_block_slot = other.current_block_slot;
// We don't need to change the current_block_slot here
// As the imported allocator's partially free slots are already accounted for during export

// Clear other allocator's states
other.current_block_slot = 0;
// Append imported memory blocks from the exported allocator
memory_blocks.insert(memory_blocks.end(),
std::make_move_iterator(exported.memory_blocks.begin()),
std::make_move_iterator(exported.memory_blocks.end()));
Comment on lines +134 to +135
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, why the move iterators for copying pointers? It's sort-of correct, as our goal is re-homing ownership of these pointers, but I bet it'll cost some compile time and not improve generated code.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, I think begin() and end() already serve the purpose well.


return *this;
// Clear the imported memory blocks
exported.memory_blocks = std::vector<pointer>();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assignment to an empty vector isn't the right way to clear this; better is the clear() method.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait - really? I thought std::move already set the T&& t to an undefined state.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but continuing to use a moved-from value has no guaranteed behavior, so it's necessary to clear it so you know what state the vector is in.

}

// Address functions
template <typename T, size_t BlockSize>
typename PoolAllocator<T, BlockSize>::pointer
PoolAllocator<T, BlockSize>::addressof(reference x) const noexcept
void
PoolAllocator<T, BlockSize>::transfer_all(PoolAllocator<T, BlockSize>& from)
{
return std::addressof(x);
assert(&from != this && "Cannot import directly from self");

// Export and Import the free slots
auto exported = from._export_all();
_import(exported);
}

template <typename T, size_t BlockSize>
typename PoolAllocator<T, BlockSize>::const_pointer
PoolAllocator<T, BlockSize>::addressof(const_reference x) const noexcept
void
PoolAllocator<T, BlockSize>::transfer_free(PoolAllocator<T, BlockSize>& from)
{
return std::addressof(x);
assert(&from != this && "Cannot import directly from self");

// Export and Import the free slots
auto exported = from._export_free();
_import(exported);
}

template <typename T, size_t BlockSize>
void
PoolAllocator<T, BlockSize>::allocateBlock()
{
// Calculate item alignment
constexpr size_type num_items = BlockSize / sizeof(T);

if (num_items < 1)
{
throw std::bad_alloc();
}

// Allocate a new block of memory
pointer new_block =
reinterpret_cast<pointer>(::operator new(BlockSize, std::align_val_t(alignof(T))));
Expand Down Expand Up @@ -152,7 +193,7 @@ PoolAllocator<T, BlockSize>::allocate(size_type n)
// Handle single object allocation
else
{
constexpr size_type num_items = BlockSize / sizeof(T);
constexpr size_type items_per_block = BlockSize / sizeof(T);

// Check free slots first
if (!free_slots.empty())
Expand All @@ -163,7 +204,7 @@ PoolAllocator<T, BlockSize>::allocate(size_type n)
return p;
}
// Check current block slot
else if (!memory_blocks.empty() && current_block_slot < num_items)
else if (!memory_blocks.empty() && current_block_slot < items_per_block)
{
// Increment by 1
pointer p = memory_blocks.back() + current_block_slot;
Expand Down
Loading