diff --git a/.vscode/settings.json b/.vscode/settings.json index 2dc3d43..727d1fb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,59 @@ { "cmake.configureArgs": [ "-DENABLE_TESTS=ON" - ] + ], + "files.associations": { + "*.tcc": "cpp", + "type_traits": "cpp", + "array": "cpp", + "atomic": "cpp", + "bit": "cpp", + "cctype": "cpp", + "chrono": "cpp", + "clocale": "cpp", + "cmath": "cpp", + "compare": "cpp", + "concepts": "cpp", + "cstdarg": "cpp", + "cstddef": "cpp", + "cstdint": "cpp", + "cstdio": "cpp", + "cstdlib": "cpp", + "ctime": "cpp", + "cwchar": "cpp", + "cwctype": "cpp", + "deque": "cpp", + "map": "cpp", + "string": "cpp", + "unordered_map": "cpp", + "vector": "cpp", + "exception": "cpp", + "algorithm": "cpp", + "functional": "cpp", + "iterator": "cpp", + "memory": "cpp", + "memory_resource": "cpp", + "numeric": "cpp", + "optional": "cpp", + "random": "cpp", + "ratio": "cpp", + "string_view": "cpp", + "system_error": "cpp", + "tuple": "cpp", + "utility": "cpp", + "fstream": "cpp", + "initializer_list": "cpp", + "iomanip": "cpp", + "iosfwd": "cpp", + "iostream": "cpp", + "istream": "cpp", + "limits": "cpp", + "new": "cpp", + "numbers": "cpp", + "ostream": "cpp", + "sstream": "cpp", + "stdexcept": "cpp", + "streambuf": "cpp", + "typeinfo": "cpp" + } } diff --git a/CMakeLists.txt b/CMakeLists.txt index 13421a7..8807f73 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -97,6 +97,7 @@ endif() # Add tests directory option(ENABLE_TESTS "Enable unit tests" OFF) if(ENABLE_TESTS) + include(CTest) enable_testing() add_subdirectory(tests) endif() diff --git a/include/pool_allocator/pool_allocator.h b/include/pool_allocator/pool_allocator.h index c92e45f..bcb4384 100644 --- a/include/pool_allocator/pool_allocator.h +++ b/include/pool_allocator/pool_allocator.h @@ -28,33 +28,145 @@ * 1. Added incomplete struct/class support (forward declaration) * 2. Changed block and slot tracking to use std::stack backed by std::vector * 3. Added unique_ptr its necessary helper functions + * 4. Added transfer_all and transfer_free functions + * + * Modifed by Eric Norige + * Changes: + * 1. Separated PoolAllocator logic cleanly into BumpAllocator and StackAllocator + * 2. Added [[nodiscard]] to export and raw pointer allocation functions */ -#include #include -#include #include -#include -#include +#include #include -#include +#include #include -template -struct ExportedAlloc +// Internal helper for lazy bump allocation of a single contiguous block +namespace pool_allocator_detail +{ +template +struct BumpBlock { + void init(Pointer start, size_t count); + void reset() noexcept; + bool empty() const noexcept; + size_t remaining() const noexcept; + Pointer allocate_one() noexcept; + // Move any remaining slots into a free list vector and reset. + template + void export_remaining(Vec& out); + + private: + Pointer next = nullptr; // next un-split slot + Pointer end = nullptr; // one-past-the-last slot in the current block +}; + +//! A basic bump allocator; uses an underlying allocator to allocate blocks and +//! bumps a pointer within that block to provide allocations. Doesn't handle deallocations at all, +//! so is best wrapped in StackAllocator. +template , size_t BlockSize = 4096> +class BumpAllocator +{ + public: + /* Member types */ + using value_type = T; using pointer = T*; - using size_type = std::size_t; + using block_pointer = T*; + using BlocksContainer = std::vector; + using FreeSlotsContainer = std::vector; + + BumpAllocator() noexcept = default; + BumpAllocator(const BumpAllocator&) = delete; + BumpAllocator(BumpAllocator&&) noexcept = default; + BumpAllocator& operator=(const BumpAllocator&) = delete; + BumpAllocator& operator=(BumpAllocator&&) noexcept = default; + explicit BumpAllocator(Alloc&& alloc) noexcept; + + [[nodiscard]] pointer allocate(size_t n = 1); + void deallocate(pointer p, size_t n = 1) noexcept; + + ~BumpAllocator() noexcept; + // Metrics + size_t allocated_bytes() const noexcept; + size_t bump_remaining() const noexcept; + // Export: move remaining bump slots to free list and move blocks out. + // New overload that returns the exported value. + [[nodiscard]] std::pair export_all(); + // Import: take ownership of blocks (for accounting and destruction). Bump remains empty. + void import_blocks(BlocksContainer&& in_blocks); + + Alloc parent; + + private: + BumpBlock bump; + BlocksContainer blocks; +}; + +//! Wraps another allocator with a stack so that single deallocations +//! can be easily returned as single allocations. +template > +class StackAllocator +{ + public: + using value_type = T; + using pointer = value_type*; + using FreeSlotsContainer = std::vector; + + StackAllocator() noexcept = default; + explicit StackAllocator(Alloc&& alloc) noexcept; + StackAllocator(const StackAllocator&) = delete; + StackAllocator(StackAllocator&&) noexcept = default; + StackAllocator& operator=(const StackAllocator&) = delete; + StackAllocator& operator=(StackAllocator&&) noexcept = default; + + [[nodiscard]] pointer allocate(size_t n = 1); + + void deallocate(pointer p, size_t n = 1) noexcept; + + // Metrics + size_t free_size() const noexcept; + + // Transfer APIs + void transfer_free(StackAllocator& from); + void transfer_all(StackAllocator& from); + + Alloc parent; + + private: + FreeSlotsContainer free_slots; +}; - // Free slots in the block - std::stack> free_slots; +// CRTP mixin that provides object helpers (construct/destroy, new/delete, make_unique) +// to any Derived that implements allocate(n) and deallocate(ptr, n). +template +class ObjectOpsMixin +{ + public: + using value_type = T; + using pointer = T*; - // Memory blocks - Optional, only used in _export_all and _import - std::vector memory_blocks; + template + static void construct(U* p, Args&&... args); + template + static void destroy(U* p) noexcept; + + [[nodiscard]] pointer new_object(); + template + [[nodiscard]] pointer new_object(Args&&... args); + void delete_object(pointer p) noexcept; + + template + [[nodiscard]] std::unique_ptr make_unique_with(Deleter del, + Args&&... args); }; +} // namespace pool_allocator_detail + +// PoolAllocator combines a bump allocator with a stack allocator template -class PoolAllocator +class PoolAllocator : public pool_allocator_detail::ObjectOpsMixin, T> { public: /* Member types */ @@ -72,47 +184,28 @@ class PoolAllocator using propagate_on_container_swap = std::true_type; using is_always_equal = std::false_type; - // /* Legacy Rebind struct */ - // template - // struct rebind - // { - // typedef PoolAllocator other; - // }; - /* Member functions */ - // Default constructor PoolAllocator() noexcept; - // No Copy constructor + ~PoolAllocator() noexcept; + // No Copy/move PoolAllocator(const PoolAllocator& other) = delete; - // No Move constructor PoolAllocator(PoolAllocator&& other) = delete; - // No Templated copy template PoolAllocator(const PoolAllocator& other) = delete; - // Destructor - ~PoolAllocator() noexcept; - - // Assignment operator - // We do not allow copy assignment for allocators PoolAllocator& operator=(const PoolAllocator& other) = delete; - // We do not allow move assignment for allocators PoolAllocator& operator=(PoolAllocator&& other) = delete; - // Address functions - pointer addressof(reference x) const noexcept; - const_pointer addressof(const_reference x) const noexcept; - - // Allocation and deallocation - pointer allocate(size_type n = 1); - void deallocate(pointer p, size_type n = 1); - - // Construct and destory functions - template - void construct(U* p, Args&&... args) noexcept; - template - void destroy(U* p) noexcept; + // Allocation and deallocation are done by allocator + pointer allocate(size_type n = 1) + { + return allocator.allocate(n); + } + void deallocate(pointer p, size_type n = 1) noexcept + { + allocator.deallocate(p, n); + } - // Maximum size of the pool + // Maximum size of an allocation from this pool size_type max_size() const noexcept; // Unique pointer support @@ -120,6 +213,8 @@ class PoolAllocator struct Deleter { // Pool address so we know which pool to use for deletion + // If a global memory pool is used, user can implement a deleter that doesn't increase size + // of unique_ptr PoolAllocator* allocator = nullptr; template void operator()(U* ptr) const noexcept; @@ -129,28 +224,33 @@ class PoolAllocator template std::unique_ptr make_unique(Args&&... args); - // Create new object with empty constructor - pointer new_object(); - - // Create new object with arguments - template - pointer new_object(Args&&... args); - - // Delete an object - void delete_object(pointer p); + // functions provided by ObjectOpsMixin + // template + // void construct(U* p, Args&&... args); + // template + // void destroy(U* p) noexcept; + // [[nodiscard]] pointer new_object(); + // template + // [[nodiscard]] pointer new_object(Args&&... args); + // void delete_object(pointer p) noexcept; // Debug helper functions // Get total allocated size - inline size_type total_allocated_size() const noexcept + inline size_type allocated_bytes() const noexcept { - return memory_blocks.size() * BlockSize; + return allocator.parent.allocated_bytes(); } - // Get total number of free slots - // Does not account for partial blocks - inline size_type total_free_slots() const noexcept + // Get total number of free slots in StackAllocator + inline size_type num_slots_available() const noexcept { - return free_slots.size(); + return allocator.free_size(); + } + + // Get number of slots in BumpBlock of BumpAllocator + inline size_type num_bump_available() const noexcept + { + return allocator.parent.bump_remaining(); } // Transfer free slots from another allocator @@ -159,34 +259,12 @@ class PoolAllocator void transfer_all(PoolAllocator& 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. - ExportedAlloc _export_free(); - - //! Export all the memory blocks + available slots - ExportedAlloc _export_all(); - - // Import - //! Import all memory blocks and free slots from an ExportedAlloc - void _import(ExportedAlloc& exported); - - // Pointer to blocks of memory - std::vector memory_blocks; - size_type current_block_slot = 0; // Current slot in the current block - - // Free list - std::stack> free_slots; - - // Number of items in one block (will be set by the constructor only) - size_type num_items; - size_type item_size; + // Compose a free-list stack allocator over a bump allocator for backing blocks. + using BumpAlloc = pool_allocator_detail::BumpAllocator, BlockSize>; + using ComboAlloc = pool_allocator_detail::StackAllocator; + + // No explicit export/import API; transfer functions call underlying allocator ops directly + ComboAlloc allocator; // owns BlockAlloc internally and free list on top }; // Operators @@ -206,4 +284,4 @@ operator!=(const PoolAllocator& a, const PoolAllocator +#include // for std::align_val_t -// Default constructor -template -PoolAllocator::PoolAllocator() noexcept +// ==== pool_allocator_detail helpers: BumpBlock, BumpAllocator, StackAllocator ==== +namespace pool_allocator_detail +{ + +template +void +BumpBlock::init(Pointer start, size_t count) { - // 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); + this->next = start; + this->end = start + count; } -// Destructor -template -PoolAllocator::~PoolAllocator() noexcept +template +void +BumpBlock::reset() noexcept { - // Free all memory blocks - for (pointer block : memory_blocks) - { - ::operator delete(block, std::align_val_t(alignof(T))); - } + next = nullptr; + end = nullptr; } -// Address functions -template -typename PoolAllocator::pointer -PoolAllocator::addressof(reference x) const noexcept +template +bool +BumpBlock::empty() const noexcept { - return std::addressof(x); + return next == end; } -template -typename PoolAllocator::const_pointer -PoolAllocator::addressof(const_reference x) const noexcept +template +size_t +BumpBlock::remaining() const noexcept { - return std::addressof(x); + return static_cast(end - next); } -template -inline ExportedAlloc -PoolAllocator::_export_free() +template +Pointer +BumpBlock::allocate_one() noexcept { - ExportedAlloc exported; - exported.free_slots = std::move(free_slots); - // Clear the free slots stack - free_slots = std::stack>(); + if (empty()) + return nullptr; + Pointer p = next; + ++next; + return p; +} - // No memory blocks to export - exported.memory_blocks = std::vector(); +template +template +void +BumpBlock::export_remaining(Vec& out) +{ + if (empty()) + return; + out.reserve(out.size() + remaining()); + while (next != end) + { + out.push_back(next++); + } + reset(); +} - return exported; +template +BumpAllocator::BumpAllocator(Alloc&& alloc) noexcept + : parent(std::forward(alloc)) +{ } -template -ExportedAlloc -PoolAllocator::_export_all() +template +typename BumpAllocator::pointer +BumpAllocator::allocate(size_t n) { - ExportedAlloc 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) + if (n != 1) + return parent.allocate(n); + if (bump.empty()) { - // 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); - } + const size_t count = BlockSize / sizeof(value_type); + block_pointer p = parent.allocate(count); + blocks.push_back(p); + bump.init(p, count); } - // 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>(); + return bump.allocate_one(); +} - // 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(); +template +void +BumpAllocator::deallocate(pointer p, size_t n) noexcept +{ + if (n != 1) + parent.deallocate(p, n); +} - // Reset the current block slot in the allocator - current_block_slot = 0; +template +BumpAllocator::~BumpAllocator() noexcept +{ + for (auto& block : blocks) + { + const size_t count = BlockSize / sizeof(value_type); + parent.deallocate(block, count); + } + blocks.clear(); + bump.reset(); +} - return exported; +template +size_t +BumpAllocator::allocated_bytes() const noexcept +{ + return blocks.size() * BlockSize; } -template -void -PoolAllocator::_import(ExportedAlloc& exported) +template +size_t +BumpAllocator::bump_remaining() const noexcept { - // Append the free slots from the exported allocator - free_slots.c.insert(free_slots.c.end(), exported.free_slots.begin(), exported.free_slots.end()); + return bump.remaining(); +} - // 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 +template +auto +BumpAllocator::export_all() -> std::pair +{ + FreeSlotsContainer out_free_slots; + out_free_slots.reserve(bump.remaining()); + bump.export_remaining(out_free_slots); - // 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())); + BlocksContainer out_blocks; + out_blocks.swap(blocks); - // Clear the imported memory blocks - exported.memory_blocks = std::vector(); + return {std::move(out_free_slots), std::move(out_blocks)}; } -template +template void -PoolAllocator::transfer_all(PoolAllocator& from) +BumpAllocator::import_blocks(BlocksContainer&& in_blocks) { - assert(&from != this && "Cannot import directly from self"); + blocks.insert(blocks.end(), in_blocks.begin(), in_blocks.end()); +} - // Export and Import the free slots - auto exported = from._export_all(); - _import(exported); +template +StackAllocator::StackAllocator(Alloc&& alloc) noexcept + : parent(std::forward(alloc)) +{ } -template +template +typename StackAllocator::pointer +StackAllocator::allocate(size_t n) +{ + if (n != 1) + return parent.allocate(n); + if (free_slots.empty()) + return parent.allocate(n); + pointer p = free_slots.back(); + free_slots.pop_back(); + return p; +} + +template void -PoolAllocator::transfer_free(PoolAllocator& from) +StackAllocator::deallocate(pointer p, size_t n) noexcept { - assert(&from != this && "Cannot import directly from self"); + if (n != 1) + { + parent.deallocate(p, n); + return; + } + free_slots.push_back(p); +} - // Export and Import the free slots - auto exported = from._export_free(); - _import(exported); +template +size_t +StackAllocator::free_size() const noexcept +{ + return free_slots.size(); } -template +template void -PoolAllocator::allocateBlock() +StackAllocator::transfer_free(StackAllocator& from) { - // Allocate a new block of memory - pointer new_block = - reinterpret_cast(::operator new(BlockSize, std::align_val_t(alignof(T)))); + if (&from == this) + return; + FreeSlotsContainer tmp; + std::swap(tmp, from.free_slots); + free_slots.insert(free_slots.end(), tmp.begin(), tmp.end()); +} - // Push the new block to the free blocks stack - memory_blocks.push_back(new_block); +template +void +StackAllocator::transfer_all(StackAllocator& from) +{ + if (&from == this) + return; + // Move free slots first + transfer_free(from); + // Then export remaining bump slots and blocks from parent and import here + auto [fs, blocks] = from.parent.export_all(); + free_slots.insert(free_slots.end(), fs.begin(), fs.end()); + parent.import_blocks(std::move(blocks)); +} - // Reset block counter - current_block_slot = 0; +// ---- ObjectOpsMixin implementations ---- +template +template +void +ObjectOpsMixin::construct(U* p, Args&&... args) +{ + new (p) U(std::forward(args)...); } -// Allocate a single object -template -typename PoolAllocator::pointer -PoolAllocator::allocate(size_type n) +template +template +void +ObjectOpsMixin::destroy(U* p) noexcept +{ + p->~U(); +} + +template +typename ObjectOpsMixin::pointer +ObjectOpsMixin::new_object() { - // Do nothing if n is 0 - if (n == 0) + auto* self = static_cast(this); + pointer p = self->allocate(1); + try { - return nullptr; + construct(p); + } + catch (...) + { + self->deallocate(p, 1); + throw; } - // For multiple objects, we revert to std::allocator - else if (n > 1) + return p; +} + +template +template +typename ObjectOpsMixin::pointer +ObjectOpsMixin::new_object(Args&&... args) +{ + auto* self = static_cast(this); + pointer p = self->allocate(1); + try { - return std::allocator().allocate(n); + construct(p, std::forward(args)...); } - // Handle single object allocation - else + catch (...) { - constexpr size_type items_per_block = BlockSize / sizeof(T); - - // Check free slots first - if (!free_slots.empty()) - { - // Pop the top slot from the free slots stack - pointer p = free_slots.top(); - free_slots.pop(); - return p; - } - // Check current block slot - else if (!memory_blocks.empty() && current_block_slot < items_per_block) - { - // Increment by 1 - pointer p = memory_blocks.back() + current_block_slot; - current_block_slot++; - return p; - } - // Allocate a new block - else - { - // Allocate a new block of memory - allocateBlock(); - pointer p = memory_blocks.back(); - // Reset current block slot - current_block_slot++; // Start from the first slot - // Return the first slot in the new block - return p; - } + self->deallocate(p, 1); + throw; } + return p; } -// Deallocate a single object -template +template void -PoolAllocator::deallocate(pointer p, size_type n) +ObjectOpsMixin::delete_object(pointer p) noexcept { - // Do nothing if n is 0 - if (n == 0) - { - return; - } - // For multiple objects, we revert to std::allocator - else if (n > 1) + auto* self = static_cast(this); + destroy(p); + self->deallocate(p, 1); +} + +template +template +std::unique_ptr +ObjectOpsMixin::make_unique_with(Deleter del, Args&&... args) +{ + auto* self = static_cast(this); + pointer raw = self->allocate(1); + try { - std::allocator().deallocate(p, n); + construct(raw, std::forward(args)...); } - // Handle single object deallocation - // We push it back to the available slots - else + catch (...) { - // Push pointer back to free slots stack - if (p != nullptr) - { - free_slots.push(p); - } + self->deallocate(raw, 1); + throw; } + return std::unique_ptr(raw, std::move(del)); } -// Construct an object in the allocated memory +} // namespace pool_allocator_detail + +// Default constructor +template +PoolAllocator::PoolAllocator() noexcept +{ + // Check block size vs T size; blocks must hold at least one T + static_assert(BlockSize / sizeof(T) > 0, "Block size is too small for the type T"); +} + +// Destructor +template +PoolAllocator::~PoolAllocator() noexcept = default; + + template -template void -PoolAllocator::construct(U* p, Args&&... args) noexcept +PoolAllocator::transfer_all(PoolAllocator& from) { - // Use placement new to construct the object in the allocated memory - new (p) U(std::forward(args)...); + assert(&from != this && "Cannot import directly from self"); + allocator.transfer_all(from.allocator); } -// Destroy an object in the allocated memory + template -template void -PoolAllocator::destroy(U* p) noexcept +PoolAllocator::transfer_free(PoolAllocator& from) { - // Call the destructor of the object - p->~U(); + assert(&from != this && "Cannot import directly from self"); + allocator.transfer_free(from.allocator); } // Maximum size of the pool @@ -298,69 +375,5 @@ template inline std::unique_ptr::Deleter> PoolAllocator::make_unique(Args&&... args) { - pointer raw = allocate(1); - try - { - // Construct the object in the allocated memory - construct(raw, std::forward(args)...); - } - catch (...) - { - deallocate(raw, 1); - throw; - } - return std::unique_ptr(raw, Deleter{this}); -} - -// Create a new object in the pool -// Default constructor -template -typename PoolAllocator::pointer -PoolAllocator::new_object() -{ - // Allocate a single object - pointer p = allocate(1); - try - { - // Construct the object in the allocated memory - construct(p); - } - catch (...) - { - deallocate(p, 1); - throw; - } - return p; -} - -// Create a new object in the pool with arguments -template -template -typename PoolAllocator::pointer -PoolAllocator::new_object(Args&&... args) -{ - // Allocate a single object - pointer p = allocate(1); - try - { - // Construct the object in the allocated memory with arguments - construct(p, std::forward(args)...); - } - catch (...) - { - deallocate(p, 1); - throw; - } - return p; -} - -// Delete an object in the pool -template -void -PoolAllocator::delete_object(pointer p) -{ - // Call the destructor of the object - destroy(p); - // Deallocate the object - deallocate(p, 1); + return this->template make_unique_with(Deleter{this}, std::forward(args)...); } diff --git a/tests/basic_type_test.cpp b/tests/basic_type_test.cpp index ee75ddd..4283b3e 100644 --- a/tests/basic_type_test.cpp +++ b/tests/basic_type_test.cpp @@ -1,6 +1,8 @@ #include #include +#include +#include // Test fundamental types // Test fixture: Memory pool of int, double and char diff --git a/tests/complex_type_test.cpp b/tests/complex_type_test.cpp index de76b77..0dad53e 100644 --- a/tests/complex_type_test.cpp +++ b/tests/complex_type_test.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include diff --git a/tests/incomplete_struct_test.cpp b/tests/incomplete_struct_test.cpp index 1fa5aef..f9fda89 100644 --- a/tests/incomplete_struct_test.cpp +++ b/tests/incomplete_struct_test.cpp @@ -1,4 +1,5 @@ #include +#include #include struct IncompleteStruct; diff --git a/tests/performance_test_random.cpp b/tests/performance_test_random.cpp index 5681431..2928352 100644 --- a/tests/performance_test_random.cpp +++ b/tests/performance_test_random.cpp @@ -5,8 +5,15 @@ #include // make sure this is at the top of your file #include #include +#include #include #include +#include +#include +#include +#include +#include +#include #include template diff --git a/tests/transfer_test.cpp b/tests/transfer_test.cpp new file mode 100644 index 0000000..b96d527 --- /dev/null +++ b/tests/transfer_test.cpp @@ -0,0 +1,421 @@ +#include "pool_allocator/pool_allocator.h" +#include +#include +#include +#include +#include +#include +#include +#include + +// Test parameters derived once here +namespace +{ +constexpr size_t kBlockSize = 64; // cacheline-sized blocks for tests +using value_type = int; +using TestAlloc = PoolAllocator; +constexpr size_t kSlotsPerBlock = kBlockSize / sizeof(value_type); + +template +constexpr T +ceil_div(T a, T b) +{ + return (a + b - 1) / b; +} + +// Make predictions about the state of the allocator after various sequences of +// allocations/deallocations Rules: 1) Allocator starts empty; 0 blocks, 0 slots and 0 bump +// available. 2) Each allocation will first try to use a free slot, then a bump space, and if +// neither is available, will allocate new blocks if needed. +// When allocating a new block, kSlotsPerBlock bump space will be made available +// 3) Each deallocation will free up a slot in the allocator +struct AllocatorPrediction +{ + size_t blocks_alloc = 0; + size_t slots_avail = 0; + size_t bump_avail = 0; + size_t bytes() const + { + return blocks_alloc * kBlockSize; + } + size_t live_alloc() const + { + return blocks_alloc * kSlotsPerBlock - slots_avail - bump_avail; + } + static AllocatorPrediction of_ta(const TestAlloc& alloc) + { + return AllocatorPrediction{alloc.allocated_bytes() / kBlockSize, + alloc.num_slots_available(), alloc.num_bump_available()}; + } + constexpr AllocatorPrediction alloc(int n) const + { + const size_t use_from_slots = std::min(n, slots_avail); + const size_t remaining1 = n - use_from_slots; + const size_t use_from_bump = std::min(remaining1, bump_avail); + const size_t remaining2 = remaining1 - use_from_bump; + const size_t blocks_added = ceil_div(remaining2, kSlotsPerBlock); + const size_t bump_added = blocks_added * kSlotsPerBlock - remaining2; + return AllocatorPrediction{blocks_alloc + blocks_added, slots_avail - use_from_slots, + bump_avail - use_from_bump + bump_added}; + } + constexpr AllocatorPrediction dealloc(int n) const + { + return AllocatorPrediction{blocks_alloc, slots_avail + n, bump_avail}; + } + auto cmp_tie() const + { + return std::tie(blocks_alloc, slots_avail, bump_avail); + } + bool operator==(const AllocatorPrediction& other) const + { + return this->cmp_tie() == other.cmp_tie(); + } +}; +std::ostream& +operator<<(std::ostream& os, const AllocatorPrediction& pred) +{ + return os << "AllocatorPrediction{blocks_alloc=" << pred.blocks_alloc + << ", slots_avail=" << pred.slots_avail << ", bump_avail=" << pred.bump_avail << "}"; +} + +struct ToFrom +{ + AllocatorPrediction to, from; +}; +constexpr ToFrom +transfer_pool(ToFrom tf) +{ + // .to, .from + return ToFrom{AllocatorPrediction{tf.to.blocks_alloc, tf.to.slots_avail + tf.from.slots_avail, + tf.to.bump_avail}, + AllocatorPrediction{tf.from.blocks_alloc, 0, tf.from.bump_avail}}; +} +constexpr ToFrom +transfer_all(ToFrom tf) +{ + // .to, .from + return ToFrom{AllocatorPrediction{tf.to.blocks_alloc + tf.from.blocks_alloc, + tf.to.slots_avail + tf.from.slots_avail + tf.from.bump_avail, + tf.to.bump_avail}, + AllocatorPrediction{0, 0, 0}}; +} + +} // namespace + +class TransferTest : public ::testing::Test +{ + protected: + TestAlloc allocator; + + void SetUp() override + { + // Reset allocator before each test + TestAlloc().transfer_all(allocator); + } +}; + +TEST_F(TransferTest, TransferToOtherAllocator) +{ + // Allocate some memory blocks + std::vector ptr_vec; + constexpr int NUM_ALLOC = 100; + for (int i = 0; i < NUM_ALLOC; i++) + ptr_vec.push_back(allocator.allocate()); + + constexpr auto pred = AllocatorPrediction().alloc(NUM_ALLOC); + EXPECT_EQ(pred, AllocatorPrediction::of_ta(allocator)); + + for (int* ptr : ptr_vec) + allocator.deallocate(ptr); + const auto pred2 = pred.dealloc(NUM_ALLOC); + EXPECT_EQ(pred2, AllocatorPrediction::of_ta(allocator)); + + // Create a new allocator to transfer to + TestAlloc destAllocator; + + EXPECT_EQ(AllocatorPrediction(), AllocatorPrediction::of_ta(destAllocator)); + + // Transfer memory from source to destination + ASSERT_NO_THROW(destAllocator.transfer_all(allocator)); + + // The original allocator should be empty + auto [to, from] = transfer_all({AllocatorPrediction(), pred2}); + EXPECT_EQ(from, AllocatorPrediction::of_ta(allocator)); + EXPECT_EQ(to, AllocatorPrediction::of_ta(destAllocator)); +} + +TEST_F(TransferTest, TransferFreeMovesOnlyFreeSlots) +{ + std::vector ptrs; + constexpr int NUM_ALLOC = 50; + constexpr int NUM_FREE = 20; + for (int i = 0; i < NUM_ALLOC; ++i) + ptrs.push_back(allocator.allocate()); + + // Free a subset + for (int i = 0; i < NUM_FREE; ++i) + allocator.deallocate(ptrs[i]); + EXPECT_EQ(allocator.num_slots_available(), NUM_FREE); + + const auto pred = AllocatorPrediction().alloc(NUM_ALLOC); + EXPECT_EQ(allocator.allocated_bytes(), pred.bytes()); + + TestAlloc dest; + EXPECT_EQ(dest.allocated_bytes(), 0); + EXPECT_EQ(dest.num_slots_available(), 0); + + // Transfer only free slots + ASSERT_NO_THROW(dest.transfer_free(allocator)); + + // Source keeps its blocks; free list emptied + EXPECT_EQ(allocator.allocated_bytes(), pred.bytes()); + EXPECT_EQ(allocator.num_slots_available(), 0); + + // Dest gets only the free slots; no blocks moved + EXPECT_EQ(dest.allocated_bytes(), 0); + EXPECT_EQ(dest.num_slots_available(), NUM_FREE); + + // Allocating from dest consumes transferred free slots + std::vector got; + for (int i = 0; i < NUM_FREE; ++i) + got.push_back(dest.allocate()); + EXPECT_EQ(dest.num_slots_available(), 0); + // Return them; free list restored + for (auto* p : got) + dest.deallocate(p); + EXPECT_EQ(dest.num_slots_available(), NUM_FREE); +} + +TEST_F(TransferTest, TransferFreeNoEffectWhenNoFreeSlots) +{ + // Allocate but don't free + for (int i = 0; i < 10; ++i) + ASSERT_NE(allocator.allocate(), nullptr); + EXPECT_EQ(allocator.num_slots_available(), 0); + + TestAlloc dest; + ASSERT_NO_THROW(dest.transfer_free(allocator)); + + EXPECT_EQ(dest.num_slots_available(), 0); + EXPECT_EQ(dest.allocated_bytes(), 0); +} + +TEST_F(TransferTest, TransferAllThenAllocateFromDestUsesTransferredSlots) +{ + // Allocate across multiple blocks + std::vector ptrs; + constexpr int NUM_ALLOC2 = 100; + for (int i = 0; i < NUM_ALLOC2; ++i) + ptrs.push_back(allocator.allocate()); + // Free all allocated pointers to move them to free list + for (auto* p : ptrs) + allocator.deallocate(p); + + const auto pred = AllocatorPrediction().alloc(NUM_ALLOC2).dealloc(NUM_ALLOC2); + EXPECT_EQ(pred, AllocatorPrediction::of_ta(allocator)); + + TestAlloc dest; + ASSERT_NO_THROW(dest.transfer_all(allocator)); + + auto [to, from] = transfer_all({AllocatorPrediction(), pred}); + EXPECT_EQ(from, AllocatorPrediction::of_ta(allocator)); + EXPECT_EQ(to, AllocatorPrediction::of_ta(dest)); + + // Consume all free slots first + std::vector got; + got.reserve(to.slots_avail); + for (size_t i = 0; i < to.slots_avail; ++i) + got.push_back(dest.allocate()); + EXPECT_EQ(dest.num_slots_available(), 0); + + // Next allocation forces a new block in dest (lazy bump), increasing allocated size + const auto before_bytes = dest.allocated_bytes(); + int* extra = dest.allocate(); + ASSERT_NE(extra, nullptr); + EXPECT_GT(dest.allocated_bytes(), before_bytes); + + // Clean up: return all + dest.deallocate(extra); + for (auto* p : got) + dest.deallocate(p); +} + +// Randomized sequence test verifying allocator state against a simple model +TEST(TransferRandomized, RandomSequenceMatchesPrediction) +{ + std::mt19937 rng(1337u); + std::uniform_int_distribution opDist(0, 9); // choose among 10 operations + constexpr int kIters = 1000; + + struct Model + { + int blocks_alloc = 0; + int slots_avail = 0; + int bump_avail = 0; + void alloc_one() + { + if (slots_avail > 0) + { + --slots_avail; + } + else if (bump_avail > 0) + { + --bump_avail; + } + else + { + ++blocks_alloc; + bump_avail = kSlotsPerBlock - 1; // consume one slot from the new block + } + } + void dealloc_one() + { + ++slots_avail; + } + void transfer_free_to(Model& to) + { + to.slots_avail += slots_avail; + slots_avail = 0; + } + void transfer_all_to(Model& to) + { + to.blocks_alloc += blocks_alloc; + to.slots_avail += slots_avail + bump_avail; + blocks_alloc = 0; + slots_avail = 0; + bump_avail = 0; + } + } mA, mB; + + TestAlloc A, B; + std::vector liveA; + std::vector liveB; + + auto check = [&](const TestAlloc& real, const Model& m) + { + EXPECT_EQ(real.allocated_bytes(), static_cast(m.blocks_alloc) * kBlockSize); + EXPECT_EQ(real.num_slots_available(), static_cast(m.slots_avail)); + EXPECT_EQ(real.num_bump_available(), static_cast(m.bump_avail)); + }; + + for (int it = 0; it < kIters; ++it) + { + int op = opDist(rng); + switch (op) + { + case 0: // alloc A + { + int* p = A.allocate(); + ASSERT_NE(p, nullptr); + liveA.push_back(p); + mA.alloc_one(); + break; + } + case 1: // alloc B + { + int* p = B.allocate(); + ASSERT_NE(p, nullptr); + liveB.push_back(p); + mB.alloc_one(); + break; + } + case 2: // dealloc A + { + if (!liveA.empty()) + { + std::uniform_int_distribution idx(0, liveA.size() - 1); + size_t i = idx(rng); + int* p = liveA[i]; + A.deallocate(p); + liveA[i] = liveA.back(); + liveA.pop_back(); + mA.dealloc_one(); + } + break; + } + case 3: // dealloc B + { + if (!liveB.empty()) + { + std::uniform_int_distribution idx(0, liveB.size() - 1); + size_t i = idx(rng); + int* p = liveB[i]; + B.deallocate(p); + liveB[i] = liveB.back(); + liveB.pop_back(); + mB.dealloc_one(); + } + break; + } + case 4: // transfer_free A -> B + { + B.transfer_free(A); + mA.transfer_free_to(mB); + break; + } + case 5: // transfer_free B -> A + { + A.transfer_free(B); + mB.transfer_free_to(mA); + break; + } + case 6: // transfer_all A -> B (only if no live allocations in A) + { + if (liveA.empty()) + { + B.transfer_all(A); + mA.transfer_all_to(mB); + } + break; + } + case 7: // transfer_all B -> A (only if no live allocations in B) + { + if (liveB.empty()) + { + A.transfer_all(B); + mB.transfer_all_to(mA); + } + break; + } + case 8: // bulk alloc 10 in A + { + for (int i = 0; i < 10; ++i) + { + int* p = A.allocate(); + ASSERT_NE(p, nullptr); + liveA.push_back(p); + mA.alloc_one(); + } + break; + } + case 9: // bulk alloc 10 in B + { + for (int i = 0; i < 10; ++i) + { + int* p = B.allocate(); + ASSERT_NE(p, nullptr); + liveB.push_back(p); + mB.alloc_one(); + } + break; + } + } + + check(A, mA); + check(B, mB); + } + + // Cleanup + for (int* p : liveA) + { + A.deallocate(p); + mA.dealloc_one(); + } + for (int* p : liveB) + { + B.deallocate(p); + mB.dealloc_one(); + } + check(A, mA); + check(B, mB); +}