Helios Engine 0.1.0
A modular ECS based data-oriented C++23 game engine
 
Loading...
Searching...
No Matches
pool_allocator.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <helios/core_pch.hpp>
4
9
10#include <algorithm>
11#include <atomic>
12#include <concepts>
13#include <cstddef>
14#include <cstdint>
15#include <cstring>
16#include <memory>
17
18namespace helios::memory {
19
20/**
21 * @brief Pool allocator for fixed-size allocations.
22 * @details Allocates objects of a fixed size from a pre-allocated pool.
23 * Extremely efficient for scenarios where many objects of the same size are allocated and deallocated frequently.
24 *
25 * Uses a lock-free free list to track available slots. Each free slot stores a pointer to the next free slot,
26 * forming a linked list through the free blocks.
27 *
28 * Supports proper deallocation and reuse of freed blocks.
29 *
30 * Uses lock-free atomic operations for allocation/deallocation,
31 * providing excellent performance in multi-threaded scenarios.
32 *
33 * @note Thread-safe with lock-free operations.
34 * All allocations must be the same size (or smaller than block_size).
35 */
36class PoolAllocator final {
37public:
38 /**
39 * @brief Creates a pool allocator sized for type T.
40 * @tparam T Type to allocate
41 * @param block_count Number of blocks to allocate
42 * @return PoolAllocator configured for type T
43 *
44 * @example
45 * @code
46 * auto pool = PoolAllocator::ForType<Entity>(1000);
47 * @endcode
48 */
49 template <typename T>
50 [[nodiscard]] static PoolAllocator ForType(size_t block_count) {
51 constexpr size_t min_alignment = alignof(void*);
52 constexpr size_t type_alignment = alignof(T);
53 constexpr size_t alignment = type_alignment > min_alignment ? type_alignment : min_alignment;
54 return {sizeof(T), block_count, alignment};
55 }
56
57 /**
58 * @brief Constructs a pool allocator.
59 * @warning Triggers assertion in next cases:
60 * - Block count is 0.
61 * - Alignment is not a power of 2.
62 * - Alignment is less than alignof(void*).
63 * @param block_size Size of each block in bytes (minimum 8 bytes for free list pointer)
64 * @param block_count Number of blocks to allocate
65 * @param alignment Alignment for each block (must be power of 2)
66 */
67 PoolAllocator(size_t block_size, size_t block_count, size_t alignment = kDefaultAlignment);
68 PoolAllocator(const PoolAllocator&) = delete;
69 PoolAllocator(PoolAllocator&& other) noexcept;
70 ~PoolAllocator() noexcept;
71
72 PoolAllocator& operator=(const PoolAllocator&) = delete;
73 PoolAllocator& operator=(PoolAllocator&& other) noexcept;
74
75 /**
76 * @brief Allocates a block from the pool.
77 * @note Size is ignored but kept for interface compatibility.
78 * @warning Triggers assertion if size exceeds block_size.
79 * @param size Number of bytes to allocate (must be <= block_size)
80 * @return AllocationResult with pointer and actual allocated size, or {nullptr, 0} on failure
81 */
82 [[nodiscard]] AllocationResult Allocate(size_t size) noexcept;
83
84 /**
85 * @brief Allocates memory for a single object of type T.
86 * @details Convenience function that allocates a single block for type T.
87 * The returned memory is uninitialized - use placement new to construct the object.
88 * @warning Triggers assertion if sizeof(T) exceeds block_size.
89 * @tparam T Type to allocate memory for
90 * @return Pointer to allocated memory, or nullptr on failure
91 *
92 * @example
93 * @code
94 * auto pool = PoolAllocator::ForType<Entity>(100);
95 * Entity* ptr = pool.Allocate<Entity>();
96 * if (ptr != nullptr) {
97 * new (ptr) Entity(42);
98 * }
99 * @endcode
100 */
101 template <typename T>
102 [[nodiscard]] T* Allocate() noexcept;
103
104 /**
105 * @brief Allocates and constructs a single object of type T.
106 * @details Convenience function that allocates memory and constructs the object in-place.
107 * @warning Triggers assertion if sizeof(T) exceeds block_size.
108 * @tparam T Type to allocate and construct
109 * @tparam Args Constructor argument types
110 * @param args Arguments to forward to T's constructor
111 * @return Pointer to constructed object, or nullptr on allocation failure
112 *
113 * @example
114 * @code
115 * auto pool = PoolAllocator::ForType<MyVec3>(100);
116 * auto* vec = pool.AllocateAndConstruct<MyVec3>(1.0f, 2.0f, 3.0f);
117 * @endcode
118 */
119 template <typename T, typename... Args>
120 requires std::constructible_from<T, Args...>
121 [[nodiscard]] T* AllocateAndConstruct(Args&&... args) noexcept(std::is_nothrow_constructible_v<T, Args...>);
122
123 /**
124 * @brief Deallocates a block back to the pool.
125 * @note Returns the block to the free list for reuse.
126 * @warning Triggers assertion if pointer does not belong to this pool.
127 * @param ptr Pointer to deallocate (must have been allocated from this pool)
128 */
129 void Deallocate(void* ptr) noexcept;
130
131 /**
132 * @brief Resets the pool, making all blocks available.
133 * @details Rebuilds the free list, invalidating all current allocations.
134 */
135 void Reset() noexcept;
136
137 /**
138 * @brief Checks if the allocator is full.
139 * @return True if all blocks are allocated
140 */
141 [[nodiscard]] bool Full() const noexcept { return free_block_count_.load(std::memory_order_relaxed) == 0; }
142 [[nodiscard]] bool Empty() const noexcept {
143 return free_block_count_.load(std::memory_order_relaxed) == block_count_;
144 }
145
146 /**
147 * @brief Checks if a pointer belongs to this pool.
148 * @param ptr Pointer to check
149 * @return True if pointer is within pool's memory range
150 */
151 [[nodiscard]] bool Owns(const void* ptr) const noexcept;
152
153 /**
154 * @brief Gets current allocator statistics.
155 * @return AllocatorStats with current usage information
156 */
157 [[nodiscard]] AllocatorStats Stats() const noexcept;
158
159 /**
160 * @brief Gets the block size.
161 * @return Block size in bytes
162 */
163 [[nodiscard]] size_t BlockSize() const noexcept { return block_size_; }
164
165 /**
166 * @brief Gets the block count.
167 * @return Total number of blocks
168 */
169 [[nodiscard]] size_t BlockCount() const noexcept { return block_count_; }
170
171 /**
172 * @brief Gets the total capacity of the pool.
173 * @return Capacity in bytes
174 */
175 [[nodiscard]] size_t Capacity() const noexcept { return capacity_; }
176
177 /**
178 * @brief Gets the number of free blocks.
179 * @return Number of blocks available for allocation
180 */
181 [[nodiscard]] size_t FreeBlockCount() const noexcept { return free_block_count_.load(std::memory_order_relaxed); }
182
183 /**
184 * @brief Gets the number of used blocks.
185 * @return Number of blocks currently allocated
186 */
187 [[nodiscard]] size_t UsedBlockCount() const noexcept {
188 return block_count_ - free_block_count_.load(std::memory_order_relaxed);
189 }
190
191private:
192 /**
193 * @brief Initializes the free list.
194 */
195 void InitializeFreeList() noexcept;
196
197 void* buffer_ = nullptr; ///< Underlying memory buffer
198 void* free_list_head_ = nullptr; ///< Head of free list
199 size_t block_size_ = 0; ///< Size of each block
200 size_t block_count_ = 0; ///< Total number of blocks
201 size_t capacity_ = 0; ///< Total capacity in bytes
202 size_t alignment_ = 0; ///< Alignment of blocks
203 std::atomic<void*> free_list_head_atomic_{nullptr}; ///< Atomic head of free list for lock-free operations
204 std::atomic<size_t> free_block_count_{0}; ///< Number of free blocks
205 std::atomic<size_t> peak_used_blocks_{0}; ///< Peak number of used blocks
206 std::atomic<size_t> total_allocations_{0}; ///< Total allocations made
207 std::atomic<size_t> total_deallocations_{0}; ///< Total deallocations made
208};
209
210inline PoolAllocator::PoolAllocator(size_t block_size, size_t block_count, size_t alignment)
211 : block_size_(std::max(block_size, sizeof(void*))), // Minimum size for free list pointer
212 block_count_(block_count),
213 alignment_(alignment),
214 free_block_count_{block_count} {
215 HELIOS_ASSERT(block_count_ > 0, "Failed to construct PoolAllocator: block_count must be greater than 0!, got '{}'",
216 block_count_);
217 HELIOS_ASSERT(IsPowerOfTwo(alignment), "Failed to construct PoolAllocator: alignment must be power of 2, got '{}'!",
218 alignment);
219 HELIOS_ASSERT(alignment >= alignof(void*),
220 "Failed to construct PoolAllocator: alignment must be at least '{}', got '{}'!", alignof(void*),
221 alignment);
222
223 // Align block size to alignment
224 block_size_ = AlignUp(block_size_, alignment_);
225 capacity_ = block_size_ * block_count_;
226
227 // Allocate aligned buffer
228 buffer_ = AlignedAlloc(alignment_, capacity_);
229 HELIOS_VERIFY(buffer_ != nullptr, "Failed to construct PoolAllocator: Allocation of buffer failed!");
230
231 InitializeFreeList();
232 free_list_head_atomic_.store(free_list_head_, std::memory_order_release);
233}
234
236 : buffer_(other.buffer_),
237 free_list_head_(other.free_list_head_),
238 block_size_(other.block_size_),
239 block_count_(other.block_count_),
240 capacity_(other.capacity_),
241 alignment_(other.alignment_),
242 free_list_head_atomic_{other.free_list_head_atomic_.load(std::memory_order_acquire)},
243 free_block_count_{other.free_block_count_.load(std::memory_order_acquire)},
244 peak_used_blocks_{other.peak_used_blocks_.load(std::memory_order_acquire)},
245 total_allocations_{other.total_allocations_.load(std::memory_order_acquire)},
246 total_deallocations_{other.total_deallocations_.load(std::memory_order_acquire)} {
247 other.buffer_ = nullptr;
248 other.free_list_head_ = nullptr;
249 other.block_size_ = 0;
250 other.block_count_ = 0;
251 other.capacity_ = 0;
252 other.alignment_ = 0;
253 other.free_list_head_atomic_.store(nullptr, std::memory_order_release);
254 other.free_block_count_.store(0, std::memory_order_release);
255 other.peak_used_blocks_.store(0, std::memory_order_release);
256 other.total_allocations_.store(0, std::memory_order_release);
257 other.total_deallocations_.store(0, std::memory_order_release);
258}
259
261 if (buffer_ != nullptr) {
262 AlignedFree(buffer_);
263 }
264}
265
267 if (this == &other) [[unlikely]] {
268 return *this;
269 }
270
271 // Free current buffer
272 if (buffer_ != nullptr) {
273 AlignedFree(buffer_);
274 }
275
276 // Move from other
277 buffer_ = other.buffer_;
278 free_list_head_ = other.free_list_head_;
279 block_size_ = other.block_size_;
280 block_count_ = other.block_count_;
281 capacity_ = other.capacity_;
282 alignment_ = other.alignment_;
283 free_list_head_atomic_.store(other.free_list_head_atomic_.load(std::memory_order_acquire), std::memory_order_release);
284 free_block_count_.store(other.free_block_count_.load(std::memory_order_acquire), std::memory_order_release);
285 peak_used_blocks_.store(other.peak_used_blocks_.load(std::memory_order_acquire), std::memory_order_release);
286 total_allocations_.store(other.total_allocations_.load(std::memory_order_acquire), std::memory_order_release);
287 total_deallocations_.store(other.total_deallocations_.load(std::memory_order_acquire), std::memory_order_release);
288
289 // Reset other
290 other.buffer_ = nullptr;
291 other.free_list_head_ = nullptr;
292 other.block_size_ = 0;
293 other.block_count_ = 0;
294 other.capacity_ = 0;
295 other.alignment_ = 0;
296 other.free_list_head_atomic_.store(nullptr, std::memory_order_release);
297 other.free_block_count_.store(0, std::memory_order_release);
298 other.peak_used_blocks_.store(0, std::memory_order_release);
299 other.total_allocations_.store(0, std::memory_order_release);
300 other.total_deallocations_.store(0, std::memory_order_release);
301
302 return *this;
303}
304
305inline AllocationResult PoolAllocator::Allocate(size_t size) noexcept {
306 if (size == 0) [[unlikely]] {
307 return {.ptr = nullptr, .allocated_size = 0};
308 }
309
310 HELIOS_ASSERT(size <= block_size_, "Failed to allocate memory: size '{}' exceeds block size '{}'!", size,
311 block_size_);
312
313 // Lock-free allocation using compare-and-swap
314 void* old_head = free_list_head_atomic_.load(std::memory_order_acquire);
315 void* new_head = nullptr;
316
317 do {
318 // Check if we have free blocks
319 if (old_head == nullptr) {
320 return {.ptr = nullptr, .allocated_size = 0};
321 }
322
323 // Read the next pointer from the old head
324 new_head = *reinterpret_cast<void**>(old_head);
325
326 // Try to atomically update the head pointer
327 } while (!free_list_head_atomic_.compare_exchange_weak(old_head, new_head, std::memory_order_release,
328 std::memory_order_acquire));
329
330 // Successfully allocated - update counters
331 free_block_count_.fetch_sub(1, std::memory_order_relaxed);
332 total_allocations_.fetch_add(1, std::memory_order_relaxed);
333
334 // Update peak usage
335 const size_t used_blocks = block_count_ - free_block_count_.load(std::memory_order_relaxed);
336 size_t current_peak = peak_used_blocks_.load(std::memory_order_acquire);
337 while (used_blocks > current_peak) {
338 if (peak_used_blocks_.compare_exchange_weak(current_peak, used_blocks, std::memory_order_release,
339 std::memory_order_acquire)) {
340 break;
341 }
342 }
343
344 return {old_head, block_size_};
345}
346
347inline void PoolAllocator::Deallocate(void* ptr) noexcept {
348 if (ptr == nullptr) [[unlikely]] {
349 return;
350 }
351
352 HELIOS_ASSERT(Owns(ptr), "Failed to deallocate memory: ptr does not belong to this pool!");
353
354 // Lock-free deallocation using compare-and-swap
355 void* old_head = free_list_head_atomic_.load(std::memory_order_acquire);
356
357 do {
358 // Set the next pointer of the block to the current head
359 *reinterpret_cast<void**>(ptr) = old_head;
360
361 // Try to atomically update the head pointer
362 } while (!free_list_head_atomic_.compare_exchange_weak(old_head, ptr, std::memory_order_release,
363 std::memory_order_acquire));
364
365 // Successfully deallocated - update counters
366 free_block_count_.fetch_add(1, std::memory_order_relaxed);
367 total_deallocations_.fetch_add(1, std::memory_order_relaxed);
368}
369
370inline void PoolAllocator::Reset() noexcept {
371 InitializeFreeList();
372 free_list_head_atomic_.store(free_list_head_, std::memory_order_release);
373 free_block_count_.store(block_count_, std::memory_order_release);
374 // Don't reset peak stats
375}
376
377inline bool PoolAllocator::Owns(const void* ptr) const noexcept {
378 if (ptr == nullptr || buffer_ == nullptr) {
379 return false;
380 }
381
382 const auto addr = reinterpret_cast<uintptr_t>(ptr);
383 const auto start = reinterpret_cast<uintptr_t>(buffer_);
384 const auto end = start + capacity_;
385
386 return addr >= start && addr < end;
387}
388
389inline AllocatorStats PoolAllocator::Stats() const noexcept {
390 const size_t free_blocks = free_block_count_.load(std::memory_order_relaxed);
391 const size_t used_blocks = block_count_ - free_blocks;
392 const size_t used_bytes = used_blocks * block_size_;
393 const size_t peak_blocks = peak_used_blocks_.load(std::memory_order_relaxed);
394 const size_t peak_bytes = peak_blocks * block_size_;
395 const size_t total_allocs = total_allocations_.load(std::memory_order_relaxed);
396 const size_t total_deallocs = total_deallocations_.load(std::memory_order_relaxed);
397
398 return {
399 .total_allocated = used_bytes,
400 .total_freed = 0,
401 .peak_usage = peak_bytes,
402 .allocation_count = used_blocks,
403 .total_allocations = total_allocs,
404 .total_deallocations = total_deallocs,
405 .alignment_waste = 0,
406 };
407}
408
409inline void PoolAllocator::InitializeFreeList() noexcept {
410 // Build free list by linking all blocks
411 auto* current = static_cast<uint8_t*>(buffer_);
412 free_list_head_ = current;
413
414 for (size_t i = 0; i < block_count_ - 1; ++i) {
415 uint8_t* next = current + block_size_;
416 *reinterpret_cast<void**>(current) = next;
417 current = next;
418 }
419
420 // Last block points to nullptr
421 *reinterpret_cast<void**>(current) = nullptr;
422
423 // Initialize atomic free list head
424 free_list_head_atomic_.store(free_list_head_, std::memory_order_release);
425}
426
427template <typename T>
428inline T* PoolAllocator::Allocate() noexcept {
429 auto result = Allocate(sizeof(T));
430 return static_cast<T*>(result.ptr);
431}
432
433template <typename T, typename... Args>
434 requires std::constructible_from<T, Args...>
435inline T* PoolAllocator::AllocateAndConstruct(Args&&... args) noexcept(std::is_nothrow_constructible_v<T, Args...>) {
436 T* ptr = Allocate<T>();
437 if (ptr != nullptr) [[likely]] {
438 std::construct_at(ptr, std::forward<Args>(args)...);
439 }
440 return ptr;
441}
442
443} // namespace helios::memory
#define HELIOS_ASSERT(condition,...)
Assertion macro that aborts execution in debug builds.
Definition assert.hpp:140
#define HELIOS_VERIFY(condition,...)
Verify macro that always checks the condition.
Definition assert.hpp:196
Pool allocator for fixed-size allocations.
PoolAllocator(const PoolAllocator &)=delete
size_t Capacity() const noexcept
Gets the total capacity of the pool.
bool Full() const noexcept
Checks if the allocator is full.
static PoolAllocator ForType(size_t block_count)
PoolAllocator(size_t block_size, size_t block_count, size_t alignment=kDefaultAlignment)
Constructs a pool allocator.
bool Owns(const void *ptr) const noexcept
Checks if a pointer belongs to this pool.
PoolAllocator & operator=(const PoolAllocator &)=delete
void Deallocate(void *ptr) noexcept
Deallocates a block back to the pool.
size_t UsedBlockCount() const noexcept
Gets the number of used blocks.
T * AllocateAndConstruct(Args &&... args) noexcept(std::is_nothrow_constructible_v< T, Args... >)
void Reset() noexcept
Resets the pool, making all blocks available.
size_t FreeBlockCount() const noexcept
Gets the number of free blocks.
size_t BlockSize() const noexcept
Gets the block size.
size_t BlockCount() const noexcept
Gets the block count.
AllocatorStats Stats() const noexcept
Gets current allocator statistics.
constexpr size_t kDefaultAlignment
Default alignment for allocations (cache line size for most modern CPUs).
void * AlignedAlloc(size_t alignment, size_t size)
Allocate memory with the specified alignment.
Definition common.hpp:30
constexpr size_t AlignUp(size_t size, size_t alignment) noexcept
Helper function to align a size up to the given alignment.
constexpr bool IsPowerOfTwo(size_t size) noexcept
Helper function to check if a size is a power of 2.
void AlignedFree(void *ptr)
Free memory allocated with AlignedAlloc.
Definition common.hpp:53
STL namespace.
Result type for allocation operations.
void * ptr
Pointer to allocated memory, nullptr on failure.
Statistics for tracking allocator usage.