From 640f924ffd0274c60fd4dbace4fe46a678483d65 Mon Sep 17 00:00:00 2001 From: Eric Norige Date: Sat, 9 Aug 2025 12:50:49 -0700 Subject: [PATCH 01/25] Refactor PoolAllocator: Simplify ExportedAlloc structure and improve memory management * Move ExportedAlloc inside PoolAllocator (is private now) * Use pointer for current_block_slot * remove num_items, item_size fields * introduce block_pointer (private) to distinguish blocks from slots * Optimize export_all, reusing export_free() * Remove unneeded move_iterators * Simplify allocate() cases --- include/pool_allocator/pool_allocator.h | 44 +++++------ include/pool_allocator/pool_allocator.tcc | 95 +++++++---------------- 2 files changed, 49 insertions(+), 90 deletions(-) diff --git a/include/pool_allocator/pool_allocator.h b/include/pool_allocator/pool_allocator.h index c92e45f..8cd01ae 100644 --- a/include/pool_allocator/pool_allocator.h +++ b/include/pool_allocator/pool_allocator.h @@ -40,19 +40,6 @@ #include #include -template -struct ExportedAlloc -{ - using pointer = T*; - using size_type = std::size_t; - - // Free slots in the block - std::stack> free_slots; - - // Memory blocks - Optional, only used in _export_all and _import - std::vector memory_blocks; -}; - template class PoolAllocator { @@ -159,34 +146,47 @@ class PoolAllocator void transfer_all(PoolAllocator& from); private: + //! A pointer to the beginning (or end) of a block + using block_pointer = T*; + //! Given a block pointer, return a "end" pointer for that block + block_pointer cur_block_end() const + { + return memory_blocks.back() + BlockSize / sizeof(T); + } + // Allocate a memory block void allocateBlock(); + struct ExportedAlloc + { + // Free slots in the block + std::stack> free_slots; + + // Memory blocks - Optional, only used in _export_all and _import + std::vector memory_blocks; + }; + // 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(); + ExportedAlloc _export_free(); //! Export all the memory blocks + available slots - ExportedAlloc _export_all(); + ExportedAlloc _export_all(); // Import //! Import all memory blocks and free slots from an ExportedAlloc - void _import(ExportedAlloc& exported); + 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 + std::vector memory_blocks; + pointer 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; }; // Operators diff --git a/include/pool_allocator/pool_allocator.tcc b/include/pool_allocator/pool_allocator.tcc index ba9effe..fa059b8 100644 --- a/include/pool_allocator/pool_allocator.tcc +++ b/include/pool_allocator/pool_allocator.tcc @@ -38,11 +38,8 @@ template PoolAllocator::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); + // 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 @@ -72,70 +69,50 @@ PoolAllocator::addressof(const_reference x) const noexcept } template -inline ExportedAlloc +typename PoolAllocator::ExportedAlloc PoolAllocator::_export_free() { - ExportedAlloc exported; - exported.free_slots = std::move(free_slots); - // Clear the free slots stack - free_slots = std::stack>(); - - // No memory blocks to export - exported.memory_blocks = std::vector(); - + ExportedAlloc exported; + // this clears this->free_slots so we can't re-export same slots + std::swap(exported.free_slots, this->free_slots); return exported; } template -ExportedAlloc +typename PoolAllocator::ExportedAlloc PoolAllocator::_export_all() { - ExportedAlloc exported; + ExportedAlloc exported = this->_export_free(); // 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 (!memory_blocks.empty()) { + const pointer end = cur_block_end(); + exported.free_slots.c.reserve(end - this->current_block_slot); // 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); - } + while (this->current_block_slot < end) + exported.free_slots.push(this->current_block_slot++); } - // 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>(); // 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(); + std::swap(exported.memory_blocks, this->memory_blocks); // Reset the current block slot in the allocator - current_block_slot = 0; + this->current_block_slot = nullptr; return exported; } template void -PoolAllocator::_import(ExportedAlloc& exported) +PoolAllocator::_import(ExportedAlloc 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()); - // 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 - // 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())); - - // Clear the imported memory blocks - exported.memory_blocks = std::vector(); + memory_blocks.insert(memory_blocks.end(), exported.memory_blocks.begin(), + exported.memory_blocks.end()); } template @@ -145,8 +122,7 @@ PoolAllocator::transfer_all(PoolAllocator& from) assert(&from != this && "Cannot import directly from self"); // Export and Import the free slots - auto exported = from._export_all(); - _import(exported); + this->_import(from._export_all()); } template @@ -156,8 +132,7 @@ PoolAllocator::transfer_free(PoolAllocator& from) assert(&from != this && "Cannot import directly from self"); // Export and Import the free slots - auto exported = from._export_free(); - _import(exported); + this->_import(from._export_free()); } template @@ -165,14 +140,14 @@ void PoolAllocator::allocateBlock() { // Allocate a new block of memory - pointer new_block = - reinterpret_cast(::operator new(BlockSize, std::align_val_t(alignof(T)))); + block_pointer new_block = + reinterpret_cast(::operator new(BlockSize, std::align_val_t(alignof(T)))); // Push the new block to the free blocks stack memory_blocks.push_back(new_block); // Reset block counter - current_block_slot = 0; + current_block_slot = new_block; } // Allocate a single object @@ -203,26 +178,12 @@ PoolAllocator::allocate(size_type n) 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 + if (memory_blocks.empty() || current_block_slot >= this->cur_block_end()) + // If no free slots, and no memory blocks, or current block is full + // Allocate a new block 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; - } } + return current_block_slot++; } // Deallocate a single object @@ -232,9 +193,7 @@ PoolAllocator::deallocate(pointer p, size_type n) { // Do nothing if n is 0 if (n == 0) - { return; - } // For multiple objects, we revert to std::allocator else if (n > 1) { From ceb58f51b927d77fedaed80588ee50477bff8f59 Mon Sep 17 00:00:00 2001 From: Eric Norige Date: Sat, 9 Aug 2025 13:00:46 -0700 Subject: [PATCH 02/25] Cleanups per AI review --- include/pool_allocator/pool_allocator.h | 25 +++++++-------------- include/pool_allocator/pool_allocator.tcc | 27 ++++++----------------- 2 files changed, 15 insertions(+), 37 deletions(-) diff --git a/include/pool_allocator/pool_allocator.h b/include/pool_allocator/pool_allocator.h index 8cd01ae..cc693d8 100644 --- a/include/pool_allocator/pool_allocator.h +++ b/include/pool_allocator/pool_allocator.h @@ -59,13 +59,6 @@ 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; @@ -85,17 +78,13 @@ class PoolAllocator // 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); + void deallocate(pointer p, size_type n = 1) noexcept; // Construct and destory functions template - void construct(U* p, Args&&... args) noexcept; + void construct(U* p, Args&&... args); template void destroy(U* p) noexcept; @@ -107,6 +96,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; @@ -117,14 +108,14 @@ class PoolAllocator std::unique_ptr make_unique(Args&&... args); // Create new object with empty constructor - pointer new_object(); + [[nodiscard]] pointer new_object(); // Create new object with arguments template - pointer new_object(Args&&... args); + [[nodiscard]] pointer new_object(Args&&... args); // Delete an object - void delete_object(pointer p); + void delete_object(pointer p) noexcept; // Debug helper functions // Get total allocated size @@ -186,7 +177,7 @@ class PoolAllocator pointer current_block_slot = 0; // Current slot in the current block // Free list - std::stack> free_slots; + std::vector free_slots; }; // Operators diff --git a/include/pool_allocator/pool_allocator.tcc b/include/pool_allocator/pool_allocator.tcc index fa059b8..d9e7da5 100644 --- a/include/pool_allocator/pool_allocator.tcc +++ b/include/pool_allocator/pool_allocator.tcc @@ -33,6 +33,7 @@ #include "pool_allocator.h" #include +#include // for std::align_val_t // Default constructor template @@ -53,20 +54,6 @@ PoolAllocator::~PoolAllocator() noexcept } } -// Address functions -template -typename PoolAllocator::pointer -PoolAllocator::addressof(reference x) const noexcept -{ - return std::addressof(x); -} - -template -typename PoolAllocator::const_pointer -PoolAllocator::addressof(const_reference x) const noexcept -{ - return std::addressof(x); -} template typename PoolAllocator::ExportedAlloc @@ -174,8 +161,8 @@ PoolAllocator::allocate(size_type n) if (!free_slots.empty()) { // Pop the top slot from the free slots stack - pointer p = free_slots.top(); - free_slots.pop(); + pointer p = free_slots.back(); + free_slots.pop_back(); return p; } if (memory_blocks.empty() || current_block_slot >= this->cur_block_end()) @@ -189,7 +176,7 @@ PoolAllocator::allocate(size_type n) // Deallocate a single object template void -PoolAllocator::deallocate(pointer p, size_type n) +PoolAllocator::deallocate(pointer p, size_type n) noexcept { // Do nothing if n is 0 if (n == 0) @@ -206,7 +193,7 @@ PoolAllocator::deallocate(pointer p, size_type n) // Push pointer back to free slots stack if (p != nullptr) { - free_slots.push(p); + free_slots.push_back(p); } } } @@ -215,7 +202,7 @@ PoolAllocator::deallocate(pointer p, size_type n) template template void -PoolAllocator::construct(U* p, Args&&... args) noexcept +PoolAllocator::construct(U* p, Args&&... args) { // Use placement new to construct the object in the allocated memory new (p) U(std::forward(args)...); @@ -316,7 +303,7 @@ PoolAllocator::new_object(Args&&... args) // Delete an object in the pool template void -PoolAllocator::delete_object(pointer p) +PoolAllocator::delete_object(pointer p) noexcept { // Call the destructor of the object destroy(p); From 6a26ff736b05de40b72832f071f87544af89753d Mon Sep 17 00:00:00 2001 From: Eric Norige Date: Sat, 9 Aug 2025 18:32:30 -0700 Subject: [PATCH 03/25] Finish stack->vector transition --- include/pool_allocator/pool_allocator.h | 2 +- include/pool_allocator/pool_allocator.tcc | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/include/pool_allocator/pool_allocator.h b/include/pool_allocator/pool_allocator.h index cc693d8..7e34d93 100644 --- a/include/pool_allocator/pool_allocator.h +++ b/include/pool_allocator/pool_allocator.h @@ -151,7 +151,7 @@ class PoolAllocator struct ExportedAlloc { // Free slots in the block - std::stack> free_slots; + std::vector free_slots; // Memory blocks - Optional, only used in _export_all and _import std::vector memory_blocks; diff --git a/include/pool_allocator/pool_allocator.tcc b/include/pool_allocator/pool_allocator.tcc index d9e7da5..170372f 100644 --- a/include/pool_allocator/pool_allocator.tcc +++ b/include/pool_allocator/pool_allocator.tcc @@ -75,10 +75,10 @@ PoolAllocator::_export_all() if (!memory_blocks.empty()) { const pointer end = cur_block_end(); - exported.free_slots.c.reserve(end - this->current_block_slot); + exported.free_slots.reserve(end - this->current_block_slot); // Convert the partially free (bump allocated) blocks to free slots while (this->current_block_slot < end) - exported.free_slots.push(this->current_block_slot++); + exported.free_slots.push_back(this->current_block_slot++); } // Move memory blocks to the exported struct @@ -95,7 +95,7 @@ void PoolAllocator::_import(ExportedAlloc 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()); + free_slots.insert(free_slots.end(), exported.free_slots.begin(), exported.free_slots.end()); // Append imported memory blocks from the exported allocator memory_blocks.insert(memory_blocks.end(), exported.memory_blocks.begin(), From f19bc807b3381b7fe1c046feb05078ba3d690c09 Mon Sep 17 00:00:00 2001 From: Eric Norige Date: Sat, 9 Aug 2025 18:33:40 -0700 Subject: [PATCH 04/25] Fix method naming to be consistent No CamelCase, no _ prefix --- include/pool_allocator/pool_allocator.h | 10 +++++----- include/pool_allocator/pool_allocator.tcc | 16 ++++++++-------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/include/pool_allocator/pool_allocator.h b/include/pool_allocator/pool_allocator.h index 7e34d93..f5fa1c8 100644 --- a/include/pool_allocator/pool_allocator.h +++ b/include/pool_allocator/pool_allocator.h @@ -146,14 +146,14 @@ class PoolAllocator } // Allocate a memory block - void allocateBlock(); + void allocate_block(); struct ExportedAlloc { // Free slots in the block std::vector free_slots; - // Memory blocks - Optional, only used in _export_all and _import + // Memory blocks - Optional, only used in export_all and import std::vector memory_blocks; }; @@ -163,14 +163,14 @@ class PoolAllocator //! 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(); + ExportedAlloc export_free(); //! Export all the memory blocks + available slots - ExportedAlloc _export_all(); + ExportedAlloc export_all(); // Import //! Import all memory blocks and free slots from an ExportedAlloc - void _import(ExportedAlloc exported); + void import(ExportedAlloc exported); // Pointer to blocks of memory std::vector memory_blocks; diff --git a/include/pool_allocator/pool_allocator.tcc b/include/pool_allocator/pool_allocator.tcc index 170372f..1d23a9f 100644 --- a/include/pool_allocator/pool_allocator.tcc +++ b/include/pool_allocator/pool_allocator.tcc @@ -57,7 +57,7 @@ PoolAllocator::~PoolAllocator() noexcept template typename PoolAllocator::ExportedAlloc -PoolAllocator::_export_free() +PoolAllocator::export_free() { ExportedAlloc exported; // this clears this->free_slots so we can't re-export same slots @@ -67,9 +67,9 @@ PoolAllocator::_export_free() template typename PoolAllocator::ExportedAlloc -PoolAllocator::_export_all() +PoolAllocator::export_all() { - ExportedAlloc exported = this->_export_free(); + ExportedAlloc exported = this->export_free(); // 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()) @@ -92,7 +92,7 @@ PoolAllocator::_export_all() template void -PoolAllocator::_import(ExportedAlloc exported) +PoolAllocator::import(ExportedAlloc exported) { // Append the free slots from the exported allocator free_slots.insert(free_slots.end(), exported.free_slots.begin(), exported.free_slots.end()); @@ -109,7 +109,7 @@ PoolAllocator::transfer_all(PoolAllocator& from) assert(&from != this && "Cannot import directly from self"); // Export and Import the free slots - this->_import(from._export_all()); + this->import(from.export_all()); } template @@ -119,12 +119,12 @@ PoolAllocator::transfer_free(PoolAllocator& from) assert(&from != this && "Cannot import directly from self"); // Export and Import the free slots - this->_import(from._export_free()); + this->import(from.export_free()); } template void -PoolAllocator::allocateBlock() +PoolAllocator::allocate_block() { // Allocate a new block of memory block_pointer new_block = @@ -168,7 +168,7 @@ PoolAllocator::allocate(size_type n) if (memory_blocks.empty() || current_block_slot >= this->cur_block_end()) // If no free slots, and no memory blocks, or current block is full // Allocate a new block - allocateBlock(); + allocate_block(); } return current_block_slot++; } From 066f33756065d2dd7797e37791d4fddc2788781f Mon Sep 17 00:00:00 2001 From: Eric Norige Date: Sat, 9 Aug 2025 18:36:29 -0700 Subject: [PATCH 05/25] Optimize imports --- include/pool_allocator/pool_allocator.h | 5 ----- tests/performance_test_random.cpp | 1 + 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/include/pool_allocator/pool_allocator.h b/include/pool_allocator/pool_allocator.h index f5fa1c8..0adaa82 100644 --- a/include/pool_allocator/pool_allocator.h +++ b/include/pool_allocator/pool_allocator.h @@ -30,14 +30,9 @@ * 3. Added unique_ptr its necessary helper functions */ -#include #include -#include #include -#include -#include #include -#include #include template diff --git a/tests/performance_test_random.cpp b/tests/performance_test_random.cpp index 5681431..8e8338a 100644 --- a/tests/performance_test_random.cpp +++ b/tests/performance_test_random.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include template From a4b93cad547180417b2eeff64693298788a3d537 Mon Sep 17 00:00:00 2001 From: Eric Norige Date: Sat, 9 Aug 2025 18:37:04 -0700 Subject: [PATCH 06/25] Avoid 0 for pointers --- include/pool_allocator/pool_allocator.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/pool_allocator/pool_allocator.h b/include/pool_allocator/pool_allocator.h index 0adaa82..2e78a7f 100644 --- a/include/pool_allocator/pool_allocator.h +++ b/include/pool_allocator/pool_allocator.h @@ -169,7 +169,7 @@ class PoolAllocator // Pointer to blocks of memory std::vector memory_blocks; - pointer current_block_slot = 0; // Current slot in the current block + pointer current_block_slot = nullptr; // Current slot in the current block // Free list std::vector free_slots; From 92d36da04e32061cd85e06838886c525474710e3 Mon Sep 17 00:00:00 2001 From: Eric Norige Date: Sat, 9 Aug 2025 18:48:43 -0700 Subject: [PATCH 07/25] Replace checks on whether there's any memory blocks with checks of whether current block_slot is null --- include/pool_allocator/pool_allocator.tcc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/pool_allocator/pool_allocator.tcc b/include/pool_allocator/pool_allocator.tcc index 1d23a9f..824b732 100644 --- a/include/pool_allocator/pool_allocator.tcc +++ b/include/pool_allocator/pool_allocator.tcc @@ -72,7 +72,7 @@ PoolAllocator::export_all() ExportedAlloc exported = this->export_free(); // 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()) + if (this->current_block_slot) { const pointer end = cur_block_end(); exported.free_slots.reserve(end - this->current_block_slot); @@ -165,7 +165,7 @@ PoolAllocator::allocate(size_type n) free_slots.pop_back(); return p; } - if (memory_blocks.empty() || current_block_slot >= this->cur_block_end()) + if (!current_block_slot || current_block_slot >= this->cur_block_end()) // If no free slots, and no memory blocks, or current block is full // Allocate a new block allocate_block(); From 4af5ad2b7053e0548aa50810c930b638a4b1a8bc Mon Sep 17 00:00:00 2001 From: Eric Norige Date: Sat, 9 Aug 2025 18:49:49 -0700 Subject: [PATCH 08/25] Add some file associations for vs code most important; .tpp is c++ code, so that pool_allocator.tpp gets highlighted properly. --- .vscode/settings.json | 56 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) 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" + } } From 56870c5004ede595ca8287c2b259b433634fbeab Mon Sep 17 00:00:00 2001 From: Eric Norige Date: Sat, 9 Aug 2025 18:50:06 -0700 Subject: [PATCH 09/25] Better documentation on current_block_list and free_slots --- include/pool_allocator/pool_allocator.h | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/include/pool_allocator/pool_allocator.h b/include/pool_allocator/pool_allocator.h index 2e78a7f..d03248d 100644 --- a/include/pool_allocator/pool_allocator.h +++ b/include/pool_allocator/pool_allocator.h @@ -169,9 +169,13 @@ class PoolAllocator // Pointer to blocks of memory std::vector memory_blocks; - pointer current_block_slot = nullptr; // Current slot in the current block - // Free list + // Pointer to the next uninitialized slot within the most recently allocated block; + // it is nullptr until the first block is allocated and advances as slots are carved + // out, effectively tracking how much of the current block has been consumed. + pointer current_block_slot = nullptr; + + // Free list; holds deallocated memory. This will be returned to caller first std::vector free_slots; }; From d5b9f9d07b1d70c31158ceabe379e2d3430a0c91 Mon Sep 17 00:00:00 2001 From: Eric Norige Date: Sat, 9 Aug 2025 18:57:27 -0700 Subject: [PATCH 10/25] Fix import to keep last memory block in place --- include/pool_allocator/pool_allocator.tcc | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/include/pool_allocator/pool_allocator.tcc b/include/pool_allocator/pool_allocator.tcc index 824b732..a9b7cdc 100644 --- a/include/pool_allocator/pool_allocator.tcc +++ b/include/pool_allocator/pool_allocator.tcc @@ -54,7 +54,6 @@ PoolAllocator::~PoolAllocator() noexcept } } - template typename PoolAllocator::ExportedAlloc PoolAllocator::export_free() @@ -97,9 +96,18 @@ PoolAllocator::import(ExportedAlloc exported) // Append the free slots from the exported allocator free_slots.insert(free_slots.end(), exported.free_slots.begin(), exported.free_slots.end()); + const int prior_last_block_idx = memory_blocks.size() - 1; // Append imported memory blocks from the exported allocator memory_blocks.insert(memory_blocks.end(), exported.memory_blocks.begin(), exported.memory_blocks.end()); + + if (prior_last_block_idx >= 0) + std::swap(memory_blocks.back(), memory_blocks[prior_last_block_idx]); + + // note: ok to leave current_block_slot as is + // if it's currently null, we need to allocate a new block in order to use it + // and if it's not null, we've restored the last memory block to what it was before so that the + // range [current_block_slot, this->cur_block_end()) is still what's waiting to be allocated. } template From 7bb168cef369d93ad636fb3f232b0f93b6e02191 Mon Sep 17 00:00:00 2001 From: Eric Norige Date: Sat, 9 Aug 2025 19:22:25 -0700 Subject: [PATCH 11/25] Factor a simple, nonstandard bump allocator out of poolallocator --- include/pool_allocator/pool_allocator.h | 67 +++++++++++++++++++---- include/pool_allocator/pool_allocator.tcc | 44 ++++----------- 2 files changed, 69 insertions(+), 42 deletions(-) diff --git a/include/pool_allocator/pool_allocator.h b/include/pool_allocator/pool_allocator.h index d03248d..93ec7d9 100644 --- a/include/pool_allocator/pool_allocator.h +++ b/include/pool_allocator/pool_allocator.h @@ -35,6 +35,60 @@ #include #include +// 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) + { + this->next = start; + this->end = start + count; + } + void reset() noexcept + { + next = nullptr; + end = nullptr; + } + bool empty() const noexcept + { + return next == end; + } + size_t remaining() const noexcept + { + return static_cast(end - next); + } + Pointer allocate_one() noexcept + { + if (empty()) + return nullptr; + Pointer p = next; + ++next; + return p; + } + // Move any remaining slots into a free list vector and reset. + template + void export_remaining(Vec& out) + { + if (empty()) + { + return; + } + out.reserve(out.size() + remaining()); + while (next != end) + { + out.push_back(next++); + } + reset(); + } + + private: + Pointer next = nullptr; // next un-split slot + Pointer end = nullptr; // one-past-the-last slot in the current block +}; +} // namespace pool_allocator_detail + template class PoolAllocator { @@ -134,11 +188,6 @@ class PoolAllocator private: //! A pointer to the beginning (or end) of a block using block_pointer = T*; - //! Given a block pointer, return a "end" pointer for that block - block_pointer cur_block_end() const - { - return memory_blocks.back() + BlockSize / sizeof(T); - } // Allocate a memory block void allocate_block(); @@ -170,12 +219,10 @@ class PoolAllocator // Pointer to blocks of memory std::vector memory_blocks; - // Pointer to the next uninitialized slot within the most recently allocated block; - // it is nullptr until the first block is allocated and advances as slots are carved - // out, effectively tracking how much of the current block has been consumed. - pointer current_block_slot = nullptr; + // Helper to lazily split the most recently allocated block into slots on demand. + pool_allocator_detail::BumpBlock bump; - // Free list; holds deallocated memory. This will be returned to caller first + // Free list; holds deallocated memory. This will be returned to callers first. std::vector free_slots; }; diff --git a/include/pool_allocator/pool_allocator.tcc b/include/pool_allocator/pool_allocator.tcc index a9b7cdc..6512985 100644 --- a/include/pool_allocator/pool_allocator.tcc +++ b/include/pool_allocator/pool_allocator.tcc @@ -69,23 +69,12 @@ typename PoolAllocator::ExportedAlloc PoolAllocator::export_all() { ExportedAlloc exported = this->export_free(); - // Before moving the free slots, unwind the partially free bump-allocation block - // Add its free slots to the exported free slots - if (this->current_block_slot) - { - const pointer end = cur_block_end(); - exported.free_slots.reserve(end - this->current_block_slot); - // Convert the partially free (bump allocated) blocks to free slots - while (this->current_block_slot < end) - exported.free_slots.push_back(this->current_block_slot++); - } + // Export any remaining un-split slots from the current block + this->bump.export_remaining(exported.free_slots); // Move memory blocks to the exported struct std::swap(exported.memory_blocks, this->memory_blocks); - // Reset the current block slot in the allocator - this->current_block_slot = nullptr; - return exported; } @@ -95,19 +84,9 @@ PoolAllocator::import(ExportedAlloc exported) { // Append the free slots from the exported allocator free_slots.insert(free_slots.end(), exported.free_slots.begin(), exported.free_slots.end()); - - const int prior_last_block_idx = memory_blocks.size() - 1; // Append imported memory blocks from the exported allocator memory_blocks.insert(memory_blocks.end(), exported.memory_blocks.begin(), exported.memory_blocks.end()); - - if (prior_last_block_idx >= 0) - std::swap(memory_blocks.back(), memory_blocks[prior_last_block_idx]); - - // note: ok to leave current_block_slot as is - // if it's currently null, we need to allocate a new block in order to use it - // and if it's not null, we've restored the last memory block to what it was before so that the - // range [current_block_slot, this->cur_block_end()) is still what's waiting to be allocated. } template @@ -140,9 +119,9 @@ PoolAllocator::allocate_block() // Push the new block to the free blocks stack memory_blocks.push_back(new_block); - - // Reset block counter - current_block_slot = new_block; + // Initialize bump state for lazy splitting of this block + constexpr size_type items_per_block = BlockSize / sizeof(T); + bump.init(new_block, items_per_block); } // Allocate a single object @@ -163,8 +142,6 @@ PoolAllocator::allocate(size_type n) // Handle single object allocation else { - constexpr size_type items_per_block = BlockSize / sizeof(T); - // Check free slots first if (!free_slots.empty()) { @@ -173,12 +150,15 @@ PoolAllocator::allocate(size_type n) free_slots.pop_back(); return p; } - if (!current_block_slot || current_block_slot >= this->cur_block_end()) - // If no free slots, and no memory blocks, or current block is full - // Allocate a new block + // Otherwise, carve from the current bump block, allocating a new one if needed + if (bump.empty()) + { allocate_block(); + } + return bump.allocate_one(); } - return current_block_slot++; + // unreachable, kept to satisfy all paths + return nullptr; } // Deallocate a single object From 0ddf6e42804f0e49bd161de1774910e3cd1eb283 Mon Sep 17 00:00:00 2001 From: Eric Norige Date: Sat, 9 Aug 2025 20:54:17 -0700 Subject: [PATCH 12/25] Add required ctest dependency within cmake this fixes problem with DartConfiguration.tcl --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) 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() From 4cf60b602f3daf727370e7932c206d3aca3fed59 Mon Sep 17 00:00:00 2001 From: Eric Norige Date: Sat, 9 Aug 2025 22:50:15 -0700 Subject: [PATCH 13/25] Rename the debug APIs --- include/pool_allocator/pool_allocator.h | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/include/pool_allocator/pool_allocator.h b/include/pool_allocator/pool_allocator.h index 93ec7d9..c636b3e 100644 --- a/include/pool_allocator/pool_allocator.h +++ b/include/pool_allocator/pool_allocator.h @@ -168,18 +168,24 @@ class PoolAllocator // 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; } // Get total number of free slots // Does not account for partial blocks - inline size_type total_free_slots() const noexcept + inline size_type num_slots_available() const noexcept { return free_slots.size(); } + // Get number of slots in bump allocator + inline size_type num_bump_available() const noexcept + { + return bump.remaining(); + } + // Transfer free slots from another allocator void transfer_free(PoolAllocator& from); // Transfer all memory blocks and free slots from another allocator From fa76205ce9d7f2aaec012bc359e22d9cffbd5028 Mon Sep 17 00:00:00 2001 From: Eric Norige Date: Sat, 9 Aug 2025 22:50:32 -0700 Subject: [PATCH 14/25] Add a bunch of tests on transferring between allocators --- tests/transfer_test.cpp | 239 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 tests/transfer_test.cpp diff --git a/tests/transfer_test.cpp b/tests/transfer_test.cpp new file mode 100644 index 0000000..617833a --- /dev/null +++ b/tests/transfer_test.cpp @@ -0,0 +1,239 @@ +#include "pool_allocator/pool_allocator.h" +#include +#include +#include +#include +#include +#include + +// Test parameters derived once here +namespace +{ +constexpr int kBlockSize = 64; // cacheline-sized blocks for tests +using value_type = int; +using TestAlloc = PoolAllocator; +constexpr int kSlotsPerBlock = kBlockSize / static_cast(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; + } + int 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 int use_from_slots = std::min(n, slots_avail); + const int remaining1 = n - use_from_slots; + const int use_from_bump = std::min(remaining1, bump_avail); + const int remaining2 = remaining1 - use_from_bump; + const int blocks_added = ceil_div(remaining2, kSlotsPerBlock); + const int 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) +{ + return ToFrom{.to = AllocatorPrediction{tf.to.blocks_alloc, + tf.to.slots_avail + tf.from.slots_avail, + tf.to.bump_avail}, + .from = AllocatorPrediction{tf.from.blocks_alloc, 0, tf.from.bump_avail}}; +} +constexpr ToFrom +transfer_all(ToFrom tf) +{ + return ToFrom{ + .to = 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}, + .from = 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({.to = AllocatorPrediction(), .from = 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({.to = AllocatorPrediction(), .from = 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 (int 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); +} From ef0888cc1c0d31f2838096e6f4bcf95f56ec1402 Mon Sep 17 00:00:00 2001 From: Eric Norige Date: Sat, 9 Aug 2025 22:58:43 -0700 Subject: [PATCH 15/25] Add fuzzing test for random sequence of operations --- tests/transfer_test.cpp | 177 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) diff --git a/tests/transfer_test.cpp b/tests/transfer_test.cpp index 617833a..fdee7b2 100644 --- a/tests/transfer_test.cpp +++ b/tests/transfer_test.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -237,3 +238,179 @@ TEST_F(TransferTest, TransferAllThenAllocateFromDestUsesTransferredSlots) 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); +} From b8e3bc1a81c32f828e1bdfa42595a6b241f5b8a5 Mon Sep 17 00:00:00 2001 From: Eric Norige Date: Sat, 9 Aug 2025 23:43:50 -0700 Subject: [PATCH 16/25] Refactor allocate/deallocate into layered, simpler classes --- include/pool_allocator/pool_allocator.h | 208 +++++++++++++++++++--- include/pool_allocator/pool_allocator.tcc | 109 ++---------- 2 files changed, 194 insertions(+), 123 deletions(-) diff --git a/include/pool_allocator/pool_allocator.h b/include/pool_allocator/pool_allocator.h index c636b3e..2a32e93 100644 --- a/include/pool_allocator/pool_allocator.h +++ b/include/pool_allocator/pool_allocator.h @@ -32,7 +32,9 @@ #include #include +#include #include +#include #include // Internal helper for lazy bump allocation of a single contiguous block @@ -87,8 +89,170 @@ struct BumpBlock Pointer next = nullptr; // next un-split slot Pointer end = nullptr; // one-past-the-last slot in the current block }; + +template , size_t BlockSize = 4096> +class BumpAllocator +{ + public: + /* Member types */ + using value_type = T; + using pointer = T*; + using block_pointer = T*; + + 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 + : block_source(std::forward(alloc)) + { + } + + pointer allocate(size_t n = 1) + { + if (n != 1) + return block_source.allocate(n); + if (bump.empty()) + { + // TODO: ensure this is appropriately aligned + const size_t count = BlockSize / sizeof(value_type); + block_pointer p = block_source.allocate(count); + blocks.push_back(p); + bump.init(p, count); + } + return bump.allocate_one(); + } + void deallocate(pointer p, size_t n = 1) noexcept + { + if (n != 1) + block_source.deallocate(p, n); + // single-slot dealloc is a no-op for bump + } + + ~BumpAllocator() noexcept + { + // Deallocate all blocks + for (auto& block : blocks) + { + const size_t count = BlockSize / sizeof(value_type); + block_source.deallocate(block, count); + } + blocks.clear(); + bump.reset(); + } + // Metrics + size_t allocated_bytes() const noexcept + { + return blocks.size() * BlockSize; + } + size_t bump_remaining() const noexcept + { + return bump.remaining(); + } + // Export: move remaining bump slots to free list and move blocks out. + template + void export_all(VecSlots& out_free_slots, VecBlocks& out_blocks) + { + bump.export_remaining(out_free_slots); + std::swap(out_blocks, blocks); + bump.reset(); + } + // Import: take ownership of blocks (for accounting and destruction). Bump remains empty. + template + void import_blocks(VecBlocks&& in_blocks) + { + blocks.insert(blocks.end(), std::make_move_iterator(in_blocks.begin()), + std::make_move_iterator(in_blocks.end())); + } + + private: + Alloc block_source; + BumpBlock bump; + std::vector blocks; +}; + +template > +class StackAllocator +{ + public: + using value_type = T; + using pointer = value_type*; + + StackAllocator() noexcept = default; + explicit StackAllocator(Alloc&& alloc) noexcept + : slot_source(std::forward(alloc)) + { + } + StackAllocator(const StackAllocator&) = delete; + StackAllocator(StackAllocator&&) noexcept = default; + StackAllocator& operator=(const StackAllocator&) = delete; + StackAllocator& operator=(StackAllocator&&) noexcept = default; + + pointer allocate(size_t n = 1) + { + if (n != 1) + return slot_source.allocate(n); + if (free_slots.empty()) + return slot_source.allocate(n); + pointer p = free_slots.back(); + free_slots.pop_back(); + return p; + } + + void deallocate(pointer p, size_t n = 1) noexcept + { + if (n != 1) + return slot_source.deallocate(p, n); + free_slots.push_back(p); + } + + // Metrics + size_t free_size() const noexcept + { + return free_slots.size(); + } + size_t underlying_allocated_bytes() const noexcept + { + return slot_source.allocated_bytes(); + } + size_t underlying_bump_remaining() const noexcept + { + return slot_source.bump_remaining(); + } + // Export/import free slots + template + void export_free(Vec& out) noexcept + { + std::swap(out, free_slots); + } + template + void import_free(Vec&& in) + { + free_slots.insert(free_slots.end(), std::make_move_iterator(in.begin()), + std::make_move_iterator(in.end())); + } + // Export/import blocks via underlying allocator + template + void export_all(VecSlots& out_slots, VecBlocks& out_blocks) + { + export_free(out_slots); + slot_source.export_all(out_slots, out_blocks); + } + template + void import_blocks(VecBlocks&& in_blocks) + { + slot_source.import_blocks(std::forward(in_blocks)); + } + + private: + Alloc slot_source; + std::vector free_slots; +}; + } // namespace pool_allocator_detail +// PoolAllocator combines a bump allocator with a stack allocator template class PoolAllocator { @@ -127,9 +291,18 @@ class PoolAllocator // We do not allow move assignment for allocators PoolAllocator& operator=(PoolAllocator&& other) = delete; - // Allocation and deallocation - pointer allocate(size_type n = 1); - void deallocate(pointer p, size_type n = 1) 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 an allocation from this pool + size_type max_size() const noexcept; // Construct and destory functions template @@ -137,9 +310,6 @@ class PoolAllocator template void destroy(U* p) noexcept; - // Maximum size of the pool - size_type max_size() const noexcept; - // Unique pointer support // Deleter struct Deleter @@ -170,20 +340,20 @@ class PoolAllocator // Get total allocated size inline size_type allocated_bytes() const noexcept { - return memory_blocks.size() * BlockSize; + return allocator.underlying_allocated_bytes(); } // Get total number of free slots // Does not account for partial blocks inline size_type num_slots_available() const noexcept { - return free_slots.size(); + return allocator.free_size(); } // Get number of slots in bump allocator inline size_type num_bump_available() const noexcept { - return bump.remaining(); + return allocator.underlying_bump_remaining(); } // Transfer free slots from another allocator @@ -192,19 +362,16 @@ class PoolAllocator void transfer_all(PoolAllocator& from); private: - //! A pointer to the beginning (or end) of a block - using block_pointer = T*; - - // Allocate a memory block - void allocate_block(); - + // 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; struct ExportedAlloc { // Free slots in the block std::vector free_slots; // Memory blocks - Optional, only used in export_all and import - std::vector memory_blocks; + std::vector memory_blocks; }; // Allocator import/export functions @@ -222,14 +389,7 @@ class PoolAllocator //! Import all memory blocks and free slots from an ExportedAlloc void import(ExportedAlloc exported); - // Pointer to blocks of memory - std::vector memory_blocks; - - // Helper to lazily split the most recently allocated block into slots on demand. - pool_allocator_detail::BumpBlock bump; - - // Free list; holds deallocated memory. This will be returned to callers first. - std::vector free_slots; + ComboAlloc allocator; // owns BlockAlloc internally and free list on top }; // Operators diff --git a/include/pool_allocator/pool_allocator.tcc b/include/pool_allocator/pool_allocator.tcc index 6512985..42813f4 100644 --- a/include/pool_allocator/pool_allocator.tcc +++ b/include/pool_allocator/pool_allocator.tcc @@ -45,22 +45,15 @@ PoolAllocator::PoolAllocator() noexcept // Destructor template -PoolAllocator::~PoolAllocator() noexcept -{ - // Free all memory blocks - for (pointer block : memory_blocks) - { - ::operator delete(block, std::align_val_t(alignof(T))); - } -} +PoolAllocator::~PoolAllocator() noexcept = default; template typename PoolAllocator::ExportedAlloc PoolAllocator::export_free() { ExportedAlloc exported; - // this clears this->free_slots so we can't re-export same slots - std::swap(exported.free_slots, this->free_slots); + // Export only free slots from the top free-list layer + allocator.export_free(exported.free_slots); return exported; } @@ -68,13 +61,9 @@ template typename PoolAllocator::ExportedAlloc PoolAllocator::export_all() { - ExportedAlloc exported = this->export_free(); - // Export any remaining un-split slots from the current block - this->bump.export_remaining(exported.free_slots); - - // Move memory blocks to the exported struct - std::swap(exported.memory_blocks, this->memory_blocks); - + ExportedAlloc exported; + // Then export all blocks and the remaining bump slots from the bump layer + allocator.export_all(exported.free_slots, exported.memory_blocks); return exported; } @@ -82,11 +71,10 @@ template void PoolAllocator::import(ExportedAlloc exported) { - // Append the free slots from the exported allocator - free_slots.insert(free_slots.end(), exported.free_slots.begin(), exported.free_slots.end()); - // Append imported memory blocks from the exported allocator - memory_blocks.insert(memory_blocks.end(), exported.memory_blocks.begin(), - exported.memory_blocks.end()); + // Import free slots into free-list layer + allocator.import_free(exported.free_slots); + // Import blocks ownership into bump layer (no bump state) + allocator.import_blocks(exported.memory_blocks); } template @@ -109,83 +97,6 @@ PoolAllocator::transfer_free(PoolAllocator& from) this->import(from.export_free()); } -template -void -PoolAllocator::allocate_block() -{ - // Allocate a new block of memory - block_pointer new_block = - reinterpret_cast(::operator new(BlockSize, std::align_val_t(alignof(T)))); - - // Push the new block to the free blocks stack - memory_blocks.push_back(new_block); - // Initialize bump state for lazy splitting of this block - constexpr size_type items_per_block = BlockSize / sizeof(T); - bump.init(new_block, items_per_block); -} - -// Allocate a single object -template -typename PoolAllocator::pointer -PoolAllocator::allocate(size_type n) -{ - // Do nothing if n is 0 - if (n == 0) - { - return nullptr; - } - // For multiple objects, we revert to std::allocator - else if (n > 1) - { - return std::allocator().allocate(n); - } - // Handle single object allocation - else - { - // Check free slots first - if (!free_slots.empty()) - { - // Pop the top slot from the free slots stack - pointer p = free_slots.back(); - free_slots.pop_back(); - return p; - } - // Otherwise, carve from the current bump block, allocating a new one if needed - if (bump.empty()) - { - allocate_block(); - } - return bump.allocate_one(); - } - // unreachable, kept to satisfy all paths - return nullptr; -} - -// Deallocate a single object -template -void -PoolAllocator::deallocate(pointer p, size_type n) noexcept -{ - // Do nothing if n is 0 - if (n == 0) - return; - // For multiple objects, we revert to std::allocator - else if (n > 1) - { - std::allocator().deallocate(p, n); - } - // Handle single object deallocation - // We push it back to the available slots - else - { - // Push pointer back to free slots stack - if (p != nullptr) - { - free_slots.push_back(p); - } - } -} - // Construct an object in the allocated memory template template From 63936dbc67af44627fc22b096af23dd66328bfec Mon Sep 17 00:00:00 2001 From: Eric Norige Date: Sat, 9 Aug 2025 23:48:44 -0700 Subject: [PATCH 17/25] IWYU-sourced header cleanups --- include/pool_allocator/pool_allocator.h | 2 +- tests/basic_type_test.cpp | 2 ++ tests/complex_type_test.cpp | 1 + tests/incomplete_struct_test.cpp | 1 + tests/performance_test_random.cpp | 6 ++++++ tests/transfer_test.cpp | 3 ++- 6 files changed, 13 insertions(+), 2 deletions(-) diff --git a/include/pool_allocator/pool_allocator.h b/include/pool_allocator/pool_allocator.h index 2a32e93..522e685 100644 --- a/include/pool_allocator/pool_allocator.h +++ b/include/pool_allocator/pool_allocator.h @@ -409,4 +409,4 @@ operator!=(const PoolAllocator& a, const PoolAllocator #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 8e8338a..2928352 100644 --- a/tests/performance_test_random.cpp +++ b/tests/performance_test_random.cpp @@ -5,9 +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 index fdee7b2..ef3b0c9 100644 --- a/tests/transfer_test.cpp +++ b/tests/transfer_test.cpp @@ -1,10 +1,11 @@ #include "pool_allocator/pool_allocator.h" #include +#include #include #include -#include #include #include +#include #include // Test parameters derived once here From 6bbd4047f1946d8409e594d607a30c1cf08819b1 Mon Sep 17 00:00:00 2001 From: Eric Norige Date: Sun, 10 Aug 2025 09:37:39 -0700 Subject: [PATCH 18/25] Move implementation of many methods to .tcc file This should make .h easier to read/understand --- include/pool_allocator/pool_allocator.h | 172 ++++---------------- include/pool_allocator/pool_allocator.tcc | 183 ++++++++++++++++++++++ 2 files changed, 212 insertions(+), 143 deletions(-) diff --git a/include/pool_allocator/pool_allocator.h b/include/pool_allocator/pool_allocator.h index 522e685..1bca29e 100644 --- a/include/pool_allocator/pool_allocator.h +++ b/include/pool_allocator/pool_allocator.h @@ -43,53 +43,23 @@ namespace pool_allocator_detail template struct BumpBlock { - void init(Pointer start, size_t count) - { - this->next = start; - this->end = start + count; - } - void reset() noexcept - { - next = nullptr; - end = nullptr; - } - bool empty() const noexcept - { - return next == end; - } - size_t remaining() const noexcept - { - return static_cast(end - next); - } - Pointer allocate_one() noexcept - { - if (empty()) - return nullptr; - Pointer p = next; - ++next; - return p; - } + 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) - { - if (empty()) - { - return; - } - out.reserve(out.size() + remaining()); - while (next != end) - { - out.push_back(next++); - } - reset(); - } + 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 { @@ -104,67 +74,21 @@ class BumpAllocator BumpAllocator(BumpAllocator&&) noexcept = default; BumpAllocator& operator=(const BumpAllocator&) = delete; BumpAllocator& operator=(BumpAllocator&&) noexcept = default; - explicit BumpAllocator(Alloc&& alloc) noexcept - : block_source(std::forward(alloc)) - { - } + explicit BumpAllocator(Alloc&& alloc) noexcept; - pointer allocate(size_t n = 1) - { - if (n != 1) - return block_source.allocate(n); - if (bump.empty()) - { - // TODO: ensure this is appropriately aligned - const size_t count = BlockSize / sizeof(value_type); - block_pointer p = block_source.allocate(count); - blocks.push_back(p); - bump.init(p, count); - } - return bump.allocate_one(); - } - void deallocate(pointer p, size_t n = 1) noexcept - { - if (n != 1) - block_source.deallocate(p, n); - // single-slot dealloc is a no-op for bump - } + pointer allocate(size_t n = 1); + void deallocate(pointer p, size_t n = 1) noexcept; - ~BumpAllocator() noexcept - { - // Deallocate all blocks - for (auto& block : blocks) - { - const size_t count = BlockSize / sizeof(value_type); - block_source.deallocate(block, count); - } - blocks.clear(); - bump.reset(); - } + ~BumpAllocator() noexcept; // Metrics - size_t allocated_bytes() const noexcept - { - return blocks.size() * BlockSize; - } - size_t bump_remaining() const noexcept - { - return bump.remaining(); - } + size_t allocated_bytes() const noexcept; + size_t bump_remaining() const noexcept; // Export: move remaining bump slots to free list and move blocks out. template - void export_all(VecSlots& out_free_slots, VecBlocks& out_blocks) - { - bump.export_remaining(out_free_slots); - std::swap(out_blocks, blocks); - bump.reset(); - } + void export_all(VecSlots& out_free_slots, VecBlocks& out_blocks); // Import: take ownership of blocks (for accounting and destruction). Bump remains empty. template - void import_blocks(VecBlocks&& in_blocks) - { - blocks.insert(blocks.end(), std::make_move_iterator(in_blocks.begin()), - std::make_move_iterator(in_blocks.end())); - } + void import_blocks(VecBlocks&& in_blocks); private: Alloc block_source; @@ -172,6 +96,8 @@ class BumpAllocator std::vector blocks; }; +//! Wraps another allocator with a stack so that single deallocations +//! can be easily returned as single allocations. template > class StackAllocator { @@ -180,70 +106,30 @@ class StackAllocator using pointer = value_type*; StackAllocator() noexcept = default; - explicit StackAllocator(Alloc&& alloc) noexcept - : slot_source(std::forward(alloc)) - { - } + explicit StackAllocator(Alloc&& alloc) noexcept; StackAllocator(const StackAllocator&) = delete; StackAllocator(StackAllocator&&) noexcept = default; StackAllocator& operator=(const StackAllocator&) = delete; StackAllocator& operator=(StackAllocator&&) noexcept = default; - pointer allocate(size_t n = 1) - { - if (n != 1) - return slot_source.allocate(n); - if (free_slots.empty()) - return slot_source.allocate(n); - pointer p = free_slots.back(); - free_slots.pop_back(); - return p; - } + pointer allocate(size_t n = 1); - void deallocate(pointer p, size_t n = 1) noexcept - { - if (n != 1) - return slot_source.deallocate(p, n); - free_slots.push_back(p); - } + void deallocate(pointer p, size_t n = 1) noexcept; // Metrics - size_t free_size() const noexcept - { - return free_slots.size(); - } - size_t underlying_allocated_bytes() const noexcept - { - return slot_source.allocated_bytes(); - } - size_t underlying_bump_remaining() const noexcept - { - return slot_source.bump_remaining(); - } + size_t free_size() const noexcept; + size_t underlying_allocated_bytes() const noexcept; + size_t underlying_bump_remaining() const noexcept; // Export/import free slots template - void export_free(Vec& out) noexcept - { - std::swap(out, free_slots); - } + void export_free(Vec& out) noexcept; template - void import_free(Vec&& in) - { - free_slots.insert(free_slots.end(), std::make_move_iterator(in.begin()), - std::make_move_iterator(in.end())); - } + void import_free(Vec&& in); // Export/import blocks via underlying allocator template - void export_all(VecSlots& out_slots, VecBlocks& out_blocks) - { - export_free(out_slots); - slot_source.export_all(out_slots, out_blocks); - } + void export_all(VecSlots& out_slots, VecBlocks& out_blocks); template - void import_blocks(VecBlocks&& in_blocks) - { - slot_source.import_blocks(std::forward(in_blocks)); - } + void import_blocks(VecBlocks&& in_blocks); private: Alloc slot_source; diff --git a/include/pool_allocator/pool_allocator.tcc b/include/pool_allocator/pool_allocator.tcc index 42813f4..eb5fb6a 100644 --- a/include/pool_allocator/pool_allocator.tcc +++ b/include/pool_allocator/pool_allocator.tcc @@ -35,6 +35,189 @@ #include #include // for std::align_val_t +// ==== pool_allocator_detail helpers: BumpBlock, BumpAllocator, StackAllocator ==== +namespace pool_allocator_detail { + +template +void BumpBlock::init(Pointer start, size_t count) +{ + this->next = start; + this->end = start + count; +} + +template +void BumpBlock::reset() noexcept +{ + next = nullptr; + end = nullptr; +} + +template +bool BumpBlock::empty() const noexcept +{ + return next == end; +} + +template +size_t BumpBlock::remaining() const noexcept +{ + return static_cast(end - next); +} + +template +Pointer BumpBlock::allocate_one() noexcept +{ + if (empty()) return nullptr; + Pointer p = next; + ++next; + return p; +} + +template +template +void BumpBlock::export_remaining(Vec& out) +{ + if (empty()) return; + out.reserve(out.size() + remaining()); + while (next != end) { + out.push_back(next++); + } + reset(); +} + +template +BumpAllocator::BumpAllocator(Alloc&& alloc) noexcept + : block_source(std::forward(alloc)) {} + +template +typename BumpAllocator::pointer +BumpAllocator::allocate(size_t n) +{ + if (n != 1) return block_source.allocate(n); + if (bump.empty()) { + const size_t count = BlockSize / sizeof(value_type); + block_pointer p = block_source.allocate(count); + blocks.push_back(p); + bump.init(p, count); + } + return bump.allocate_one(); +} + +template +void BumpAllocator::deallocate(pointer p, size_t n) noexcept +{ + if (n != 1) block_source.deallocate(p, n); +} + +template +BumpAllocator::~BumpAllocator() noexcept +{ + for (auto& block : blocks) { + const size_t count = BlockSize / sizeof(value_type); + block_source.deallocate(block, count); + } + blocks.clear(); + bump.reset(); +} + +template +size_t BumpAllocator::allocated_bytes() const noexcept +{ + return blocks.size() * BlockSize; +} + +template +size_t BumpAllocator::bump_remaining() const noexcept +{ + return bump.remaining(); +} + +template +template +void BumpAllocator::export_all(VecSlots& out_free_slots, VecBlocks& out_blocks) +{ + bump.export_remaining(out_free_slots); + std::swap(out_blocks, blocks); + bump.reset(); +} + +template +template +void BumpAllocator::import_blocks(VecBlocks&& in_blocks) +{ + blocks.insert(blocks.end(), std::make_move_iterator(in_blocks.begin()), + std::make_move_iterator(in_blocks.end())); +} + +template +StackAllocator::StackAllocator(Alloc&& alloc) noexcept + : slot_source(std::forward(alloc)) {} + +template +typename StackAllocator::pointer +StackAllocator::allocate(size_t n) +{ + if (n != 1) return slot_source.allocate(n); + if (free_slots.empty()) return slot_source.allocate(n); + pointer p = free_slots.back(); + free_slots.pop_back(); + return p; +} + +template +void StackAllocator::deallocate(pointer p, size_t n) noexcept +{ + if (n != 1) { slot_source.deallocate(p, n); return; } + free_slots.push_back(p); +} + +template +size_t StackAllocator::free_size() const noexcept { return free_slots.size(); } + +template +size_t StackAllocator::underlying_allocated_bytes() const noexcept +{ + return slot_source.allocated_bytes(); +} + +template +size_t StackAllocator::underlying_bump_remaining() const noexcept +{ + return slot_source.bump_remaining(); +} + +template +template +void StackAllocator::export_free(Vec& out) noexcept +{ + std::swap(out, free_slots); +} + +template +template +void StackAllocator::import_free(Vec&& in) +{ + free_slots.insert(free_slots.end(), std::make_move_iterator(in.begin()), + std::make_move_iterator(in.end())); +} + +template +template +void StackAllocator::export_all(VecSlots& out_slots, VecBlocks& out_blocks) +{ + export_free(out_slots); + slot_source.export_all(out_slots, out_blocks); +} + +template +template +void StackAllocator::import_blocks(VecBlocks&& in_blocks) +{ + slot_source.import_blocks(std::forward(in_blocks)); +} + +} // namespace pool_allocator_detail + // Default constructor template PoolAllocator::PoolAllocator() noexcept From 9430ceebc60a77dc4db8869a14befd3f07c3c4c2 Mon Sep 17 00:00:00 2001 From: Eric Norige Date: Sun, 10 Aug 2025 22:09:01 -0700 Subject: [PATCH 19/25] Expose in a uniform way the parent allocator of Stack/BumpAllocator --- include/pool_allocator/pool_allocator.h | 4 ++-- include/pool_allocator/pool_allocator.tcc | 26 +++++++++++------------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/include/pool_allocator/pool_allocator.h b/include/pool_allocator/pool_allocator.h index 1bca29e..a48d09d 100644 --- a/include/pool_allocator/pool_allocator.h +++ b/include/pool_allocator/pool_allocator.h @@ -90,8 +90,8 @@ class BumpAllocator template void import_blocks(VecBlocks&& in_blocks); + Alloc parent; private: - Alloc block_source; BumpBlock bump; std::vector blocks; }; @@ -131,8 +131,8 @@ class StackAllocator template void import_blocks(VecBlocks&& in_blocks); + Alloc parent; private: - Alloc slot_source; std::vector free_slots; }; diff --git a/include/pool_allocator/pool_allocator.tcc b/include/pool_allocator/pool_allocator.tcc index eb5fb6a..d3997a8 100644 --- a/include/pool_allocator/pool_allocator.tcc +++ b/include/pool_allocator/pool_allocator.tcc @@ -87,16 +87,16 @@ void BumpBlock::export_remaining(Vec& out) template BumpAllocator::BumpAllocator(Alloc&& alloc) noexcept - : block_source(std::forward(alloc)) {} + : parent(std::forward(alloc)) {} template typename BumpAllocator::pointer BumpAllocator::allocate(size_t n) { - if (n != 1) return block_source.allocate(n); + if (n != 1) return parent.allocate(n); if (bump.empty()) { const size_t count = BlockSize / sizeof(value_type); - block_pointer p = block_source.allocate(count); + block_pointer p = parent.allocate(count); blocks.push_back(p); bump.init(p, count); } @@ -106,7 +106,7 @@ BumpAllocator::allocate(size_t n) template void BumpAllocator::deallocate(pointer p, size_t n) noexcept { - if (n != 1) block_source.deallocate(p, n); + if (n != 1) parent.deallocate(p, n); } template @@ -114,7 +114,7 @@ BumpAllocator::~BumpAllocator() noexcept { for (auto& block : blocks) { const size_t count = BlockSize / sizeof(value_type); - block_source.deallocate(block, count); + parent.deallocate(block, count); } blocks.clear(); bump.reset(); @@ -151,14 +151,14 @@ void BumpAllocator::import_blocks(VecBlocks&& in_blocks) template StackAllocator::StackAllocator(Alloc&& alloc) noexcept - : slot_source(std::forward(alloc)) {} + : parent(std::forward(alloc)) {} template typename StackAllocator::pointer StackAllocator::allocate(size_t n) { - if (n != 1) return slot_source.allocate(n); - if (free_slots.empty()) return slot_source.allocate(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; @@ -167,7 +167,7 @@ StackAllocator::allocate(size_t n) template void StackAllocator::deallocate(pointer p, size_t n) noexcept { - if (n != 1) { slot_source.deallocate(p, n); return; } + if (n != 1) { parent.deallocate(p, n); return; } free_slots.push_back(p); } @@ -177,13 +177,13 @@ size_t StackAllocator::free_size() const noexcept { return free_slots. template size_t StackAllocator::underlying_allocated_bytes() const noexcept { - return slot_source.allocated_bytes(); + return parent.allocated_bytes(); } template size_t StackAllocator::underlying_bump_remaining() const noexcept { - return slot_source.bump_remaining(); + return parent.bump_remaining(); } template @@ -206,14 +206,14 @@ template void StackAllocator::export_all(VecSlots& out_slots, VecBlocks& out_blocks) { export_free(out_slots); - slot_source.export_all(out_slots, out_blocks); + parent.export_all(out_slots, out_blocks); } template template void StackAllocator::import_blocks(VecBlocks&& in_blocks) { - slot_source.import_blocks(std::forward(in_blocks)); + parent.import_blocks(std::forward(in_blocks)); } } // namespace pool_allocator_detail From 9cbd0d92484a17aa4e393f2ae55a09e4bb6fa155 Mon Sep 17 00:00:00 2001 From: Eric Norige Date: Sun, 10 Aug 2025 22:12:41 -0700 Subject: [PATCH 20/25] Remove indirect stats; access sub-allocators to get stats directly --- include/pool_allocator/pool_allocator.h | 11 ++++------- include/pool_allocator/pool_allocator.tcc | 12 ------------ 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/include/pool_allocator/pool_allocator.h b/include/pool_allocator/pool_allocator.h index a48d09d..5078ba3 100644 --- a/include/pool_allocator/pool_allocator.h +++ b/include/pool_allocator/pool_allocator.h @@ -118,8 +118,6 @@ class StackAllocator // Metrics size_t free_size() const noexcept; - size_t underlying_allocated_bytes() const noexcept; - size_t underlying_bump_remaining() const noexcept; // Export/import free slots template void export_free(Vec& out) noexcept; @@ -226,20 +224,19 @@ class PoolAllocator // Get total allocated size inline size_type allocated_bytes() const noexcept { - return allocator.underlying_allocated_bytes(); + return allocator.parent.allocated_bytes(); } - // Get total number of free slots - // Does not account for partial blocks + // Get total number of free slots in StackAllocator inline size_type num_slots_available() const noexcept { return allocator.free_size(); } - // Get number of slots in bump allocator + // Get number of slots in BumpBlock of BumpAllocator inline size_type num_bump_available() const noexcept { - return allocator.underlying_bump_remaining(); + return allocator.parent.bump_remaining(); } // Transfer free slots from another allocator diff --git a/include/pool_allocator/pool_allocator.tcc b/include/pool_allocator/pool_allocator.tcc index d3997a8..7ae7876 100644 --- a/include/pool_allocator/pool_allocator.tcc +++ b/include/pool_allocator/pool_allocator.tcc @@ -174,18 +174,6 @@ void StackAllocator::deallocate(pointer p, size_t n) noexcept template size_t StackAllocator::free_size() const noexcept { return free_slots.size(); } -template -size_t StackAllocator::underlying_allocated_bytes() const noexcept -{ - return parent.allocated_bytes(); -} - -template -size_t StackAllocator::underlying_bump_remaining() const noexcept -{ - return parent.bump_remaining(); -} - template template void StackAllocator::export_free(Vec& out) noexcept From 6485b0e2b071c83c653c7d54922b0b41acc42f42 Mon Sep 17 00:00:00 2001 From: Eric Norige Date: Sun, 10 Aug 2025 22:13:17 -0700 Subject: [PATCH 21/25] Reformat code; NFC --- include/pool_allocator/pool_allocator.h | 2 + include/pool_allocator/pool_allocator.tcc | 98 ++++++++++++++++------- 2 files changed, 70 insertions(+), 30 deletions(-) diff --git a/include/pool_allocator/pool_allocator.h b/include/pool_allocator/pool_allocator.h index 5078ba3..f01f0f9 100644 --- a/include/pool_allocator/pool_allocator.h +++ b/include/pool_allocator/pool_allocator.h @@ -91,6 +91,7 @@ class BumpAllocator void import_blocks(VecBlocks&& in_blocks); Alloc parent; + private: BumpBlock bump; std::vector blocks; @@ -130,6 +131,7 @@ class StackAllocator void import_blocks(VecBlocks&& in_blocks); Alloc parent; + private: std::vector free_slots; }; diff --git a/include/pool_allocator/pool_allocator.tcc b/include/pool_allocator/pool_allocator.tcc index 7ae7876..c016d36 100644 --- a/include/pool_allocator/pool_allocator.tcc +++ b/include/pool_allocator/pool_allocator.tcc @@ -36,38 +36,45 @@ #include // for std::align_val_t // ==== pool_allocator_detail helpers: BumpBlock, BumpAllocator, StackAllocator ==== -namespace pool_allocator_detail { +namespace pool_allocator_detail +{ template -void BumpBlock::init(Pointer start, size_t count) +void +BumpBlock::init(Pointer start, size_t count) { this->next = start; this->end = start + count; } template -void BumpBlock::reset() noexcept +void +BumpBlock::reset() noexcept { next = nullptr; end = nullptr; } template -bool BumpBlock::empty() const noexcept +bool +BumpBlock::empty() const noexcept { return next == end; } template -size_t BumpBlock::remaining() const noexcept +size_t +BumpBlock::remaining() const noexcept { return static_cast(end - next); } template -Pointer BumpBlock::allocate_one() noexcept +Pointer +BumpBlock::allocate_one() noexcept { - if (empty()) return nullptr; + if (empty()) + return nullptr; Pointer p = next; ++next; return p; @@ -75,11 +82,14 @@ Pointer BumpBlock::allocate_one() noexcept template template -void BumpBlock::export_remaining(Vec& out) +void +BumpBlock::export_remaining(Vec& out) { - if (empty()) return; + if (empty()) + return; out.reserve(out.size() + remaining()); - while (next != end) { + while (next != end) + { out.push_back(next++); } reset(); @@ -87,14 +97,18 @@ void BumpBlock::export_remaining(Vec& out) template BumpAllocator::BumpAllocator(Alloc&& alloc) noexcept - : parent(std::forward(alloc)) {} + : parent(std::forward(alloc)) +{ +} template typename BumpAllocator::pointer BumpAllocator::allocate(size_t n) { - if (n != 1) return parent.allocate(n); - if (bump.empty()) { + if (n != 1) + return parent.allocate(n); + if (bump.empty()) + { const size_t count = BlockSize / sizeof(value_type); block_pointer p = parent.allocate(count); blocks.push_back(p); @@ -104,15 +118,18 @@ BumpAllocator::allocate(size_t n) } template -void BumpAllocator::deallocate(pointer p, size_t n) noexcept +void +BumpAllocator::deallocate(pointer p, size_t n) noexcept { - if (n != 1) parent.deallocate(p, n); + if (n != 1) + parent.deallocate(p, n); } template BumpAllocator::~BumpAllocator() noexcept { - for (auto& block : blocks) { + for (auto& block : blocks) + { const size_t count = BlockSize / sizeof(value_type); parent.deallocate(block, count); } @@ -121,20 +138,23 @@ BumpAllocator::~BumpAllocator() noexcept } template -size_t BumpAllocator::allocated_bytes() const noexcept +size_t +BumpAllocator::allocated_bytes() const noexcept { return blocks.size() * BlockSize; } template -size_t BumpAllocator::bump_remaining() const noexcept +size_t +BumpAllocator::bump_remaining() const noexcept { return bump.remaining(); } template template -void BumpAllocator::export_all(VecSlots& out_free_slots, VecBlocks& out_blocks) +void +BumpAllocator::export_all(VecSlots& out_free_slots, VecBlocks& out_blocks) { bump.export_remaining(out_free_slots); std::swap(out_blocks, blocks); @@ -143,7 +163,8 @@ void BumpAllocator::export_all(VecSlots& out_free_slots, Ve template template -void BumpAllocator::import_blocks(VecBlocks&& in_blocks) +void +BumpAllocator::import_blocks(VecBlocks&& in_blocks) { blocks.insert(blocks.end(), std::make_move_iterator(in_blocks.begin()), std::make_move_iterator(in_blocks.end())); @@ -151,39 +172,54 @@ void BumpAllocator::import_blocks(VecBlocks&& in_blocks) template StackAllocator::StackAllocator(Alloc&& alloc) noexcept - : parent(std::forward(alloc)) {} + : parent(std::forward(alloc)) +{ +} template typename StackAllocator::pointer StackAllocator::allocate(size_t n) { - if (n != 1) return parent.allocate(n); - if (free_slots.empty()) return parent.allocate(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 StackAllocator::deallocate(pointer p, size_t n) noexcept +void +StackAllocator::deallocate(pointer p, size_t n) noexcept { - if (n != 1) { parent.deallocate(p, n); return; } + if (n != 1) + { + parent.deallocate(p, n); + return; + } free_slots.push_back(p); } template -size_t StackAllocator::free_size() const noexcept { return free_slots.size(); } +size_t +StackAllocator::free_size() const noexcept +{ + return free_slots.size(); +} template template -void StackAllocator::export_free(Vec& out) noexcept +void +StackAllocator::export_free(Vec& out) noexcept { std::swap(out, free_slots); } template template -void StackAllocator::import_free(Vec&& in) +void +StackAllocator::import_free(Vec&& in) { free_slots.insert(free_slots.end(), std::make_move_iterator(in.begin()), std::make_move_iterator(in.end())); @@ -191,7 +227,8 @@ void StackAllocator::import_free(Vec&& in) template template -void StackAllocator::export_all(VecSlots& out_slots, VecBlocks& out_blocks) +void +StackAllocator::export_all(VecSlots& out_slots, VecBlocks& out_blocks) { export_free(out_slots); parent.export_all(out_slots, out_blocks); @@ -199,7 +236,8 @@ void StackAllocator::export_all(VecSlots& out_slots, VecBlocks& out_bl template template -void StackAllocator::import_blocks(VecBlocks&& in_blocks) +void +StackAllocator::import_blocks(VecBlocks&& in_blocks) { parent.import_blocks(std::forward(in_blocks)); } From d66f8e017339e98cd16573ada179c277a7209945 Mon Sep 17 00:00:00 2001 From: Eric Norige Date: Sun, 10 Aug 2025 23:02:33 -0700 Subject: [PATCH 22/25] Implement object operations of PoolAllocator in a mixin --- include/pool_allocator/pool_allocator.h | 67 +++++---- include/pool_allocator/pool_allocator.tcc | 167 +++++++++++----------- 2 files changed, 122 insertions(+), 112 deletions(-) diff --git a/include/pool_allocator/pool_allocator.h b/include/pool_allocator/pool_allocator.h index f01f0f9..567f45b 100644 --- a/include/pool_allocator/pool_allocator.h +++ b/include/pool_allocator/pool_allocator.h @@ -76,7 +76,7 @@ class BumpAllocator BumpAllocator& operator=(BumpAllocator&&) noexcept = default; explicit BumpAllocator(Alloc&& alloc) noexcept; - pointer allocate(size_t n = 1); + [[nodiscard]] pointer allocate(size_t n = 1); void deallocate(pointer p, size_t n = 1) noexcept; ~BumpAllocator() noexcept; @@ -113,7 +113,7 @@ class StackAllocator StackAllocator& operator=(const StackAllocator&) = delete; StackAllocator& operator=(StackAllocator&&) noexcept = default; - pointer allocate(size_t n = 1); + [[nodiscard]] pointer allocate(size_t n = 1); void deallocate(pointer p, size_t n = 1) noexcept; @@ -136,11 +136,35 @@ class StackAllocator std::vector 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*; + + 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 */ @@ -159,24 +183,17 @@ class PoolAllocator using is_always_equal = std::false_type; /* 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; + // Allocation and deallocation are done by allocator pointer allocate(size_type n = 1) { @@ -190,12 +207,6 @@ class PoolAllocator // Maximum size of an allocation from this pool size_type max_size() const noexcept; - // Construct and destory functions - template - void construct(U* p, Args&&... args); - template - void destroy(U* p) noexcept; - // Unique pointer support // Deleter struct Deleter @@ -212,15 +223,15 @@ class PoolAllocator template std::unique_ptr make_unique(Args&&... args); - // Create new object with empty constructor - [[nodiscard]] pointer new_object(); - - // Create new object with arguments - template - [[nodiscard]] pointer new_object(Args&&... args); - - // Delete an object - void delete_object(pointer p) noexcept; + // 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 diff --git a/include/pool_allocator/pool_allocator.tcc b/include/pool_allocator/pool_allocator.tcc index c016d36..ba38753 100644 --- a/include/pool_allocator/pool_allocator.tcc +++ b/include/pool_allocator/pool_allocator.tcc @@ -242,6 +242,88 @@ StackAllocator::import_blocks(VecBlocks&& in_blocks) parent.import_blocks(std::forward(in_blocks)); } +// ---- ObjectOpsMixin implementations ---- +template +template +void +ObjectOpsMixin::construct(U* p, Args&&... args) +{ + new (p) U(std::forward(args)...); +} + +template +template +void +ObjectOpsMixin::destroy(U* p) noexcept +{ + p->~U(); +} + +template +typename ObjectOpsMixin::pointer +ObjectOpsMixin::new_object() +{ + auto* self = static_cast(this); + pointer p = self->allocate(1); + try + { + construct(p); + } + catch (...) + { + self->deallocate(p, 1); + throw; + } + return p; +} + +template +template +typename ObjectOpsMixin::pointer +ObjectOpsMixin::new_object(Args&&... args) +{ + auto* self = static_cast(this); + pointer p = self->allocate(1); + try + { + construct(p, std::forward(args)...); + } + catch (...) + { + self->deallocate(p, 1); + throw; + } + return p; +} + +template +void +ObjectOpsMixin::delete_object(pointer p) noexcept +{ + 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 + { + construct(raw, std::forward(args)...); + } + catch (...) + { + self->deallocate(raw, 1); + throw; + } + return std::unique_ptr(raw, std::move(del)); +} + } // namespace pool_allocator_detail // Default constructor @@ -306,25 +388,6 @@ PoolAllocator::transfer_free(PoolAllocator& from) this->import(from.export_free()); } -// Construct an object in the allocated memory -template -template -void -PoolAllocator::construct(U* p, Args&&... args) -{ - // Use placement new to construct the object in the allocated memory - new (p) U(std::forward(args)...); -} -// Destroy an object in the allocated memory -template -template -void -PoolAllocator::destroy(U* p) noexcept -{ - // Call the destructor of the object - p->~U(); -} - // Maximum size of the pool template typename PoolAllocator::size_type @@ -352,69 +415,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) noexcept -{ - // 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)...); } From 2585414b6e6a3acb1e85fc1af394d2add1d11e2c Mon Sep 17 00:00:00 2001 From: Eric Norige Date: Sun, 10 Aug 2025 23:31:26 -0700 Subject: [PATCH 23/25] Refactor import/export to be more value oriented (less non-const ref args) --- include/pool_allocator/pool_allocator.h | 48 ++++++++--------- include/pool_allocator/pool_allocator.tcc | 63 +++++++++++------------ 2 files changed, 55 insertions(+), 56 deletions(-) diff --git a/include/pool_allocator/pool_allocator.h b/include/pool_allocator/pool_allocator.h index 567f45b..5c1d698 100644 --- a/include/pool_allocator/pool_allocator.h +++ b/include/pool_allocator/pool_allocator.h @@ -68,6 +68,8 @@ class BumpAllocator using value_type = T; using pointer = T*; using block_pointer = T*; + using BlocksContainer = std::vector; + using FreeSlotsContainer = std::vector; BumpAllocator() noexcept = default; BumpAllocator(const BumpAllocator&) = delete; @@ -84,8 +86,11 @@ class BumpAllocator size_t allocated_bytes() const noexcept; size_t bump_remaining() const noexcept; // Export: move remaining bump slots to free list and move blocks out. - template - void export_all(VecSlots& out_free_slots, VecBlocks& out_blocks); + // New overload that returns the exported value. + [[nodiscard]] std::pair export_all(); + // // Backward-compatible overload that fills the provided containers. + // template + // void export_all(VecSlots& out_free_slots, VecBlocks& out_blocks); // Import: take ownership of blocks (for accounting and destruction). Bump remains empty. template void import_blocks(VecBlocks&& in_blocks); @@ -94,7 +99,7 @@ class BumpAllocator private: BumpBlock bump; - std::vector blocks; + BlocksContainer blocks; }; //! Wraps another allocator with a stack so that single deallocations @@ -105,6 +110,7 @@ class StackAllocator public: using value_type = T; using pointer = value_type*; + using FreeSlotsContainer = std::vector; StackAllocator() noexcept = default; explicit StackAllocator(Alloc&& alloc) noexcept; @@ -119,21 +125,26 @@ class StackAllocator // Metrics size_t free_size() const noexcept; + + struct ExportedAlloc + { + // Free slots in the block + FreeSlotsContainer free_slots; + + // Memory blocks - Optional, only used in export_all and import + typename Alloc::BlocksContainer memory_blocks; + }; // Export/import free slots - template - void export_free(Vec& out) noexcept; - template - void import_free(Vec&& in); + FreeSlotsContainer export_free() noexcept; + void import_free(FreeSlotsContainer&& in); // Export/import blocks via underlying allocator - template - void export_all(VecSlots& out_slots, VecBlocks& out_blocks); - template - void import_blocks(VecBlocks&& in_blocks); + ExportedAlloc export_all(); + void import_blocks(ExportedAlloc&& in_blocks); Alloc parent; private: - std::vector free_slots; + FreeSlotsContainer free_slots; }; // CRTP mixin that provides object helpers (construct/destroy, new/delete, make_unique) @@ -193,7 +204,6 @@ class PoolAllocator : public pool_allocator_detail::ObjectOpsMixin, BlockSize>; using ComboAlloc = pool_allocator_detail::StackAllocator; - struct ExportedAlloc - { - // Free slots in the block - std::vector free_slots; - - // Memory blocks - Optional, only used in export_all and import - std::vector memory_blocks; - }; - + using ExportedAlloc = typename ComboAlloc::ExportedAlloc; // Allocator import/export functions // Export //! Export only the available slots as a vector of pointers. @@ -283,7 +285,7 @@ class PoolAllocator : public pool_allocator_detail::ObjectOpsMixin::bump_remaining() const noexcept } template -template -void -BumpAllocator::export_all(VecSlots& out_free_slots, VecBlocks& out_blocks) +auto +BumpAllocator::export_all() -> std::pair { + FreeSlotsContainer out_free_slots; + out_free_slots.reserve(bump.remaining()); bump.export_remaining(out_free_slots); - std::swap(out_blocks, blocks); - bump.reset(); + + BlocksContainer out_blocks; + out_blocks.swap(blocks); + + return {std::move(out_free_slots), std::move(out_blocks)}; } template @@ -209,37 +213,39 @@ StackAllocator::free_size() const noexcept } template -template -void -StackAllocator::export_free(Vec& out) noexcept +typename StackAllocator::FreeSlotsContainer +StackAllocator::export_free() noexcept { + FreeSlotsContainer out; std::swap(out, free_slots); + return out; } template -template void -StackAllocator::import_free(Vec&& in) +StackAllocator::import_free(FreeSlotsContainer&& in) { - free_slots.insert(free_slots.end(), std::make_move_iterator(in.begin()), - std::make_move_iterator(in.end())); + free_slots.insert(free_slots.end(), in.begin(), in.end()); } template -template -void -StackAllocator::export_all(VecSlots& out_slots, VecBlocks& out_blocks) +typename StackAllocator::ExportedAlloc +StackAllocator::export_all() { - export_free(out_slots); - parent.export_all(out_slots, out_blocks); + ExportedAlloc exported; + exported.free_slots = export_free(); + auto [fs, mb] = parent.export_all(); + exported.memory_blocks = std::move(mb); + exported.free_slots.insert(exported.free_slots.end(), fs.begin(), fs.end()); + return exported; } template -template void -StackAllocator::import_blocks(VecBlocks&& in_blocks) +StackAllocator::import_blocks(ExportedAlloc&& in_blocks) { - parent.import_blocks(std::forward(in_blocks)); + import_free(std::move(in_blocks.free_slots)); + parent.import_blocks(std::move(in_blocks.memory_blocks)); } // ---- ObjectOpsMixin implementations ---- @@ -342,30 +348,21 @@ template typename PoolAllocator::ExportedAlloc PoolAllocator::export_free() { - ExportedAlloc exported; - // Export only free slots from the top free-list layer - allocator.export_free(exported.free_slots); - return exported; + return ExportedAlloc{.free_slots = allocator.export_free()}; } template typename PoolAllocator::ExportedAlloc PoolAllocator::export_all() { - ExportedAlloc exported; - // Then export all blocks and the remaining bump slots from the bump layer - allocator.export_all(exported.free_slots, exported.memory_blocks); - return exported; + return allocator.export_all(); } template void -PoolAllocator::import(ExportedAlloc exported) +PoolAllocator::import(ExportedAlloc&& exported) { - // Import free slots into free-list layer - allocator.import_free(exported.free_slots); - // Import blocks ownership into bump layer (no bump state) - allocator.import_blocks(exported.memory_blocks); + allocator.import_blocks(std::move(exported)); } template From 821354c754e3afdf003922936addf38bd32443d5 Mon Sep 17 00:00:00 2001 From: Eric Norige Date: Sun, 10 Aug 2025 23:50:10 -0700 Subject: [PATCH 24/25] Move transfer API up allocator hierarchy --- include/pool_allocator/pool_allocator.h | 39 ++---------- include/pool_allocator/pool_allocator.tcc | 75 ++++++----------------- 2 files changed, 24 insertions(+), 90 deletions(-) diff --git a/include/pool_allocator/pool_allocator.h b/include/pool_allocator/pool_allocator.h index 5c1d698..f1f5eeb 100644 --- a/include/pool_allocator/pool_allocator.h +++ b/include/pool_allocator/pool_allocator.h @@ -88,12 +88,8 @@ class BumpAllocator // Export: move remaining bump slots to free list and move blocks out. // New overload that returns the exported value. [[nodiscard]] std::pair export_all(); - // // Backward-compatible overload that fills the provided containers. - // template - // void export_all(VecSlots& out_free_slots, VecBlocks& out_blocks); // Import: take ownership of blocks (for accounting and destruction). Bump remains empty. - template - void import_blocks(VecBlocks&& in_blocks); + void import_blocks(BlocksContainer&& in_blocks); Alloc parent; @@ -126,20 +122,9 @@ class StackAllocator // Metrics size_t free_size() const noexcept; - struct ExportedAlloc - { - // Free slots in the block - FreeSlotsContainer free_slots; - - // Memory blocks - Optional, only used in export_all and import - typename Alloc::BlocksContainer memory_blocks; - }; - // Export/import free slots - FreeSlotsContainer export_free() noexcept; - void import_free(FreeSlotsContainer&& in); - // Export/import blocks via underlying allocator - ExportedAlloc export_all(); - void import_blocks(ExportedAlloc&& in_blocks); + // Transfer APIs + void transfer_free(StackAllocator& from); + void transfer_all(StackAllocator& from); Alloc parent; @@ -271,22 +256,8 @@ class PoolAllocator : public pool_allocator_detail::ObjectOpsMixin, BlockSize>; using ComboAlloc = pool_allocator_detail::StackAllocator; - using ExportedAlloc = typename ComboAlloc::ExportedAlloc; - // 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); + // No explicit export/import API; transfer functions call underlying allocator ops directly ComboAlloc allocator; // owns BlockAlloc internally and free list on top }; diff --git a/include/pool_allocator/pool_allocator.tcc b/include/pool_allocator/pool_allocator.tcc index 3970b57..84c0f24 100644 --- a/include/pool_allocator/pool_allocator.tcc +++ b/include/pool_allocator/pool_allocator.tcc @@ -166,12 +166,10 @@ BumpAllocator::export_all() -> std::pair -template void -BumpAllocator::import_blocks(VecBlocks&& in_blocks) +BumpAllocator::import_blocks(BlocksContainer&& in_blocks) { - blocks.insert(blocks.end(), std::make_move_iterator(in_blocks.begin()), - std::make_move_iterator(in_blocks.end())); + blocks.insert(blocks.end(), in_blocks.begin(), in_blocks.end()); } template @@ -212,40 +210,29 @@ StackAllocator::free_size() const noexcept return free_slots.size(); } -template -typename StackAllocator::FreeSlotsContainer -StackAllocator::export_free() noexcept -{ - FreeSlotsContainer out; - std::swap(out, free_slots); - return out; -} - template void -StackAllocator::import_free(FreeSlotsContainer&& in) -{ - free_slots.insert(free_slots.end(), in.begin(), in.end()); -} - -template -typename StackAllocator::ExportedAlloc -StackAllocator::export_all() +StackAllocator::transfer_free(StackAllocator& from) { - ExportedAlloc exported; - exported.free_slots = export_free(); - auto [fs, mb] = parent.export_all(); - exported.memory_blocks = std::move(mb); - exported.free_slots.insert(exported.free_slots.end(), fs.begin(), fs.end()); - return exported; + if (&from == this) + return; + FreeSlotsContainer tmp; + std::swap(tmp, from.free_slots); + free_slots.insert(free_slots.end(), tmp.begin(), tmp.end()); } template void -StackAllocator::import_blocks(ExportedAlloc&& in_blocks) +StackAllocator::transfer_all(StackAllocator& from) { - import_free(std::move(in_blocks.free_slots)); - parent.import_blocks(std::move(in_blocks.memory_blocks)); + 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)); } // ---- ObjectOpsMixin implementations ---- @@ -344,35 +331,13 @@ PoolAllocator::PoolAllocator() noexcept template PoolAllocator::~PoolAllocator() noexcept = default; -template -typename PoolAllocator::ExportedAlloc -PoolAllocator::export_free() -{ - return ExportedAlloc{.free_slots = allocator.export_free()}; -} - -template -typename PoolAllocator::ExportedAlloc -PoolAllocator::export_all() -{ - return allocator.export_all(); -} - -template -void -PoolAllocator::import(ExportedAlloc&& exported) -{ - allocator.import_blocks(std::move(exported)); -} template void PoolAllocator::transfer_all(PoolAllocator& from) { assert(&from != this && "Cannot import directly from self"); - - // Export and Import the free slots - this->import(from.export_all()); + allocator.transfer_all(from.allocator); } template @@ -380,9 +345,7 @@ void PoolAllocator::transfer_free(PoolAllocator& from) { assert(&from != this && "Cannot import directly from self"); - - // Export and Import the free slots - this->import(from.export_free()); + allocator.transfer_free(from.allocator); } // Maximum size of the pool From e5d02ae2dab28c572b6c00d414a5401e4d1b06f1 Mon Sep 17 00:00:00 2001 From: Canlin Zhang Date: Mon, 11 Aug 2025 20:47:56 +0000 Subject: [PATCH 25/25] Minor fixes * Fix compilation error on MSVC due to desginated initializer issue * Fix type mismatch in tests --- include/pool_allocator/pool_allocator.h | 6 + tests/transfer_test.cpp | 218 ++++++++++++------------ 2 files changed, 117 insertions(+), 107 deletions(-) diff --git a/include/pool_allocator/pool_allocator.h b/include/pool_allocator/pool_allocator.h index f1f5eeb..bcb4384 100644 --- a/include/pool_allocator/pool_allocator.h +++ b/include/pool_allocator/pool_allocator.h @@ -28,6 +28,12 @@ * 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 diff --git a/tests/transfer_test.cpp b/tests/transfer_test.cpp index ef3b0c9..b96d527 100644 --- a/tests/transfer_test.cpp +++ b/tests/transfer_test.cpp @@ -11,10 +11,10 @@ // Test parameters derived once here namespace { -constexpr int kBlockSize = 64; // cacheline-sized blocks for tests +constexpr size_t kBlockSize = 64; // cacheline-sized blocks for tests using value_type = int; using TestAlloc = PoolAllocator; -constexpr int kSlotsPerBlock = kBlockSize / static_cast(sizeof(value_type)); +constexpr size_t kSlotsPerBlock = kBlockSize / sizeof(value_type); template constexpr T @@ -38,7 +38,7 @@ struct AllocatorPrediction { return blocks_alloc * kBlockSize; } - int live_alloc() const + size_t live_alloc() const { return blocks_alloc * kSlotsPerBlock - slots_avail - bump_avail; } @@ -49,12 +49,12 @@ struct AllocatorPrediction } constexpr AllocatorPrediction alloc(int n) const { - const int use_from_slots = std::min(n, slots_avail); - const int remaining1 = n - use_from_slots; - const int use_from_bump = std::min(remaining1, bump_avail); - const int remaining2 = remaining1 - use_from_bump; - const int blocks_added = ceil_div(remaining2, kSlotsPerBlock); - const int bump_added = blocks_added * kSlotsPerBlock - remaining2; + 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}; } @@ -85,19 +85,19 @@ struct ToFrom constexpr ToFrom transfer_pool(ToFrom tf) { - return ToFrom{.to = AllocatorPrediction{tf.to.blocks_alloc, - tf.to.slots_avail + tf.from.slots_avail, - tf.to.bump_avail}, - .from = AllocatorPrediction{tf.from.blocks_alloc, 0, tf.from.bump_avail}}; + // .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) { - return ToFrom{ - .to = 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}, - .from = AllocatorPrediction{0, 0, 0}}; + // .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 @@ -139,7 +139,7 @@ TEST_F(TransferTest, TransferToOtherAllocator) ASSERT_NO_THROW(destAllocator.transfer_all(allocator)); // The original allocator should be empty - auto [to, from] = transfer_all({.to = AllocatorPrediction(), .from = pred2}); + auto [to, from] = transfer_all({AllocatorPrediction(), pred2}); EXPECT_EQ(from, AllocatorPrediction::of_ta(allocator)); EXPECT_EQ(to, AllocatorPrediction::of_ta(destAllocator)); } @@ -217,14 +217,14 @@ TEST_F(TransferTest, TransferAllThenAllocateFromDestUsesTransferredSlots) TestAlloc dest; ASSERT_NO_THROW(dest.transfer_all(allocator)); - auto [to, from] = transfer_all({.to = AllocatorPrediction(), .from = pred}); + 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 (int i = 0; i < to.slots_avail; ++i) + for (size_t i = 0; i < to.slots_avail; ++i) got.push_back(dest.allocate()); EXPECT_EQ(dest.num_slots_available(), 0); @@ -268,7 +268,10 @@ TEST(TransferRandomized, RandomSequenceMatchesPrediction) bump_avail = kSlotsPerBlock - 1; // consume one slot from the new block } } - void dealloc_one() { ++slots_avail; } + void dealloc_one() + { + ++slots_avail; + } void transfer_free_to(Model& to) { to.slots_avail += slots_avail; @@ -288,7 +291,8 @@ TEST(TransferRandomized, RandomSequenceMatchesPrediction) std::vector liveA; std::vector liveB; - auto check = [&](const TestAlloc& real, const Model& m) { + 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)); @@ -299,102 +303,102 @@ TEST(TransferRandomized, RandomSequenceMatchesPrediction) 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 + 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()) { - B.transfer_free(A); - mA.transfer_free_to(mB); - break; + 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(); } - case 5: // transfer_free B -> A + break; + } + case 3: // dealloc B + { + if (!liveB.empty()) { - A.transfer_free(B); - mB.transfer_free_to(mA); - break; + 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(); } - case 6: // transfer_all A -> B (only if no live allocations in A) + 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()) { - if (liveA.empty()) - { - B.transfer_all(A); - mA.transfer_all_to(mB); - } - break; + B.transfer_all(A); + mA.transfer_all_to(mB); } - case 7: // transfer_all B -> A (only if no live allocations in B) + break; + } + case 7: // transfer_all B -> A (only if no live allocations in B) + { + if (liveB.empty()) { - if (liveB.empty()) - { - A.transfer_all(B); - mB.transfer_all_to(mA); - } - break; + A.transfer_all(B); + mB.transfer_all_to(mA); } - case 8: // bulk alloc 10 in A + break; + } + case 8: // bulk alloc 10 in A + { + for (int i = 0; i < 10; ++i) { - for (int i = 0; i < 10; ++i) - { - int* p = A.allocate(); - ASSERT_NE(p, nullptr); - liveA.push_back(p); - mA.alloc_one(); - } - break; + int* p = A.allocate(); + ASSERT_NE(p, nullptr); + liveA.push_back(p); + mA.alloc_one(); } - case 9: // bulk alloc 10 in B + break; + } + case 9: // bulk alloc 10 in B + { + for (int i = 0; i < 10; ++i) { - for (int i = 0; i < 10; ++i) - { - int* p = B.allocate(); - ASSERT_NE(p, nullptr); - liveB.push_back(p); - mB.alloc_one(); - } - break; + int* p = B.allocate(); + ASSERT_NE(p, nullptr); + liveB.push_back(p); + mB.alloc_one(); } + break; + } } check(A, mA);