Helios Engine
A modular ECS based data-oriented C++23 game engine framework
Loading...
Searching...
No Matches
pool_allocator.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <helios/assert.hpp>
5#include <helios/memory/details/profile.hpp>
6
7#include <atomic>
8#include <cstddef>
9#include <cstdint>
10#include <memory_resource>
11
12namespace helios::mem {
13
14/// @brief Configuration for PoolAllocator.
16 /// @brief Minimum payload size of each pool block in bytes.
17 size_t block_size = 0;
18 /// @brief Number of blocks in the initial chunk.
19 size_t block_count = 0;
20 /// @brief Alignment of each block in bytes; must be a power of two.
22 /// @brief Growth policy applied when additional chunks are required.
24};
25
26/**
27 * @brief Lock-free PMR pool allocator for fixed-size blocks.
28 * @details Allocates from fixed-size blocks stored in growable chunks.
29 *
30 * Fast path uses lock-free freelist push/pop based on embedded next pointers.
31 * Growth allocates and links a new chunk, then pushes all its blocks into the
32 * freelist.
33 */
34class PoolAllocator final : public std::pmr::memory_resource {
35public:
36 /**
37 * @brief Constructs pool allocator from options.
38 * @warning Triggers assertion when block_size or block_count is zero, when
39 * alignment is not a power of two, or when alignment is less than
40 * `alignof(void*)`.
41 * @param options Pool allocator options
42 */
43 explicit PoolAllocator(PoolAllocatorOptions options) noexcept;
44
45 /**
46 * @brief Constructs pool allocator with geometric growth.
47 * @warning Triggers assertion when `block_size` or `block_count` is zero,
48 * when `alignment` is not a power of two, or when `alignment` is less than
49 * `alignof(void*)`.
50 * @param block_size Size of each block in bytes
51 * @param block_count Number of blocks in initial chunk
52 * @param alignment Block alignment
53 */
54 PoolAllocator(size_t block_size, size_t block_count,
55 size_t alignment = kDefaultAlignment) noexcept
57 PoolAllocatorOptions{.block_size = block_size,
58 .block_count = block_count,
59 .alignment = alignment,
60 .growth = GrowthPolicy::Geometric()}) {}
61
62 PoolAllocator(const PoolAllocator&) = delete;
63
64 /**
65 * @brief Move-constructs pool allocator from another instance.
66 * @param other Source allocator; left in empty moved-from state
67 */
68 PoolAllocator(PoolAllocator&& other) noexcept { MoveFrom(other); }
69 ~PoolAllocator() noexcept override {
70 FreeChunkChain(chunks_.load(std::memory_order_acquire));
71 }
72
74
75 /**
76 * @brief Move-assigns pool state from another instance.
77 * @param other Source allocator; left in empty moved-from state
78 * @return Reference to this allocator
79 */
80 PoolAllocator& operator=(PoolAllocator&& other) noexcept;
81
82 /**
83 * @brief Helper for constructing pool allocator for specific type.
84 * @tparam T Type to pool
85 * @param block_count Number of blocks in initial chunk
86 * @return Configured pool allocator
87 */
88 template <typename T>
89 [[nodiscard]] static PoolAllocator ForType(size_t block_count) noexcept;
90
91 /**
92 * @brief Resets pool to fully free state.
93 * @warning Must not be called concurrently with allocate/deallocate.
94 */
95 void Reset() noexcept;
96
97 /**
98 * @brief Returns true when no free blocks remain.
99 * @return True if full, false otherwise
100 */
101 [[nodiscard]] bool Full() const noexcept {
102 return free_blocks_.load(std::memory_order_relaxed) == 0;
103 }
104
105 /**
106 * @brief Returns true when all blocks are free.
107 * @return True if empty, false otherwise
108 */
109 [[nodiscard]] bool Empty() const noexcept {
110 return free_blocks_.load(std::memory_order_relaxed) ==
111 total_blocks_.load(std::memory_order_relaxed);
112 }
113
114 /**
115 * @brief Checks pointer ownership.
116 * @param ptr Pointer to test
117 * @return True when pointer belongs to any pool chunk, false otherwise
118 */
119 [[nodiscard]] bool Owns(const void* ptr) const noexcept;
120
121 /**
122 * @brief Returns runtime stats.
123 * @return Allocator stats
124 */
125 [[nodiscard]] AllocatorStats Stats() const noexcept;
126
127 /**
128 * @brief Returns effective block size.
129 * @return Block size in bytes
130 */
131 [[nodiscard]] size_t BlockSize() const noexcept { return block_size_; }
132
133 /**
134 * @brief Returns configured block alignment.
135 * @return Alignment in bytes
136 */
137 [[nodiscard]] size_t Alignment() const noexcept { return alignment_; }
138
139 /**
140 * @brief Returns number of blocks in first chunk.
141 * @return Initial block count
142 */
143 [[nodiscard]] size_t InitialBlockCount() const noexcept {
144 return initial_block_count_;
145 }
146
147 /**
148 * @brief Returns total blocks across chunks.
149 * @return Total block count
150 */
151 [[nodiscard]] size_t BlockCount() const noexcept {
152 return total_blocks_.load(std::memory_order_relaxed);
153 }
154
155 /**
156 * @brief Returns free block count.
157 * @return Free block count
158 */
159 [[nodiscard]] size_t FreeBlockCount() const noexcept {
160 return free_blocks_.load(std::memory_order_relaxed);
161 }
162
163 /**
164 * @brief Returns used block count.
165 * @return Used block count
166 */
167 [[nodiscard]] size_t UsedBlockCount() const noexcept;
168
169 /**
170 * @brief Returns growth policy.
171 * @return Growth policy
172 */
173 [[nodiscard]] GrowthPolicy Growth() const noexcept { return growth_; }
174
175private:
176 struct ChunkHeader {
177 void* buffer = nullptr;
178 size_t capacity = 0;
179 size_t block_count = 0;
180 std::atomic<ChunkHeader*> next{nullptr};
181 };
182
183 enum class GrowState : uint8_t {
184 kIdle,
185 kGrowing,
186 };
187
188 [[nodiscard]] static ChunkHeader* CreateChunk(size_t block_size,
189 size_t block_count,
190 size_t alignment) noexcept;
191 void FreeChunkChain(ChunkHeader* chunk) noexcept;
192
193 [[nodiscard]] bool GrowIfNeeded() noexcept;
194 void PushChunkBlocks(ChunkHeader& chunk) noexcept;
195 void RebuildFreeList() noexcept;
196 void MoveFrom(PoolAllocator& other) noexcept;
197
198 [[nodiscard]] void* do_allocate(size_t bytes, size_t alignment) override;
199 void do_deallocate(void* ptr, size_t bytes, size_t alignment) override;
200 [[nodiscard]] bool do_is_equal(
201 const std::pmr::memory_resource& other) const noexcept override {
202 return this == &other;
203 }
204
205 size_t block_size_ = 0;
206 size_t initial_block_count_ = 0;
207 size_t alignment_ = 0;
208 GrowthPolicy growth_;
209
210 std::atomic<void*> free_head_{nullptr};
211 std::atomic<ChunkHeader*> chunks_{nullptr};
212 std::atomic<GrowState> grow_state_{GrowState::kIdle};
213
214 std::atomic<size_t> total_blocks_{0};
215 std::atomic<size_t> free_blocks_{0};
216 std::atomic<size_t> peak_used_blocks_{0};
217 std::atomic<size_t> total_allocations_{0};
218 std::atomic<size_t> total_deallocations_{0};
219};
220
221template <typename T>
222inline PoolAllocator PoolAllocator::ForType(size_t block_count) noexcept {
223 constexpr size_t type_align =
224 alignof(T) > alignof(void*) ? alignof(T) : alignof(void*);
225 return {sizeof(T), block_count, type_align};
226}
227
229 if (this == &other) [[unlikely]] {
230 return *this;
231 }
232
233 FreeChunkChain(chunks_.load(std::memory_order_acquire));
234 MoveFrom(other);
235 return *this;
236}
237
238inline void PoolAllocator::Reset() noexcept {
239 HELIOS_MEMORY_PROFILE_SCOPE_N("helios::mem::PoolAllocator::Reset");
240
241 RebuildFreeList();
242 total_allocations_.store(0, std::memory_order_release);
243 total_deallocations_.store(0, std::memory_order_release);
244}
245
246inline auto PoolAllocator::Stats() const noexcept -> AllocatorStats {
247 const size_t total = total_blocks_.load(std::memory_order_relaxed);
248 const size_t free = free_blocks_.load(std::memory_order_relaxed);
249 const size_t used = total >= free ? total - free : 0;
250 const size_t peak = peak_used_blocks_.load(std::memory_order_relaxed);
251
252 return {
253 .total_allocated = SaturatingMul(used, block_size_),
254 .peak_usage = SaturatingMul(peak, block_size_),
255 .allocation_count = used,
256 .total_allocations = total_allocations_.load(std::memory_order_relaxed),
257 .total_deallocations =
258 total_deallocations_.load(std::memory_order_relaxed),
259 .alignment_waste = 0,
260 };
261}
262
263inline size_t PoolAllocator::UsedBlockCount() const noexcept {
264 const size_t total = total_blocks_.load(std::memory_order_relaxed);
265 const size_t free = free_blocks_.load(std::memory_order_relaxed);
266 return total >= free ? total - free : 0;
267}
268
269} // namespace helios::mem
static PoolAllocator ForType(size_t block_count) noexcept
Helper for constructing pool allocator for specific type.
PoolAllocator(PoolAllocator &&other) noexcept
Move-constructs pool allocator from another instance.
GrowthPolicy Growth() const noexcept
Returns growth policy.
AllocatorStats Stats() const noexcept
Returns runtime stats.
PoolAllocator & operator=(const PoolAllocator &)=delete
bool Empty() const noexcept
Returns true when all blocks are free.
PoolAllocator(size_t block_size, size_t block_count, size_t alignment=kDefaultAlignment) noexcept
Constructs pool allocator with geometric growth.
size_t Alignment() const noexcept
Returns configured block alignment.
bool Full() const noexcept
Returns true when no free blocks remain.
size_t UsedBlockCount() const noexcept
Returns used block count.
size_t FreeBlockCount() const noexcept
Returns free block count.
void Reset() noexcept
Resets pool to fully free state.
size_t InitialBlockCount() const noexcept
Returns number of blocks in first chunk.
size_t BlockSize() const noexcept
Returns effective block size.
PoolAllocator(PoolAllocatorOptions options) noexcept
Constructs pool allocator from options.
PoolAllocator(const PoolAllocator &)=delete
size_t BlockCount() const noexcept
Returns total blocks across chunks.
bool Owns(const void *ptr) const noexcept
Checks pointer ownership.
~PoolAllocator() noexcept override
constexpr size_t SaturatingMul(size_t lhs, size_t rhs) noexcept
Saturating multiply for size_t.
Definition common.hpp:39
constexpr size_t kDefaultAlignment
Default alignment used by memory resources.
Definition common.hpp:14
Runtime statistics snapshot for PMR allocators.
Definition common.hpp:163
Capacity growth configuration.
Definition common.hpp:60
static constexpr GrowthPolicy Geometric(size_t numerator=2, size_t denominator=1, size_t max=std::numeric_limits< size_t >::max()) noexcept
Creates a geometric growth policy.
Definition common.hpp:96
Configuration for PoolAllocator.
size_t block_count
Number of blocks in the initial chunk.
GrowthPolicy growth
Growth policy applied when additional chunks are required.
size_t alignment
Alignment of each block in bytes; must be a power of two.
size_t block_size
Minimum payload size of each pool block in bytes.