Helios Engine
A modular ECS based data-oriented C++23 game engine framework
Loading...
Searching...
No Matches
pool_allocator.cpp
Go to the documentation of this file.
1#include <pch.hpp>
2
4
5#include <details/accumulate_peak.hpp>
6#include <helios/assert.hpp>
9#include <helios/memory/details/profile.hpp>
10
11#include <algorithm>
12#include <atomic>
13#include <cstddef>
14#include <cstdint>
15#include <memory>
16#include <utility>
17
18namespace {
19
20void PushBlock(std::atomic<void*>& head, void* block) noexcept {
21 void* observed = head.load(std::memory_order_acquire);
22 for (;;) {
23 *static_cast<void**>(block) = observed;
24 if (head.compare_exchange_weak(observed, block, std::memory_order_release,
25 std::memory_order_acquire)) {
26 return;
27 }
28 }
29}
30
31[[nodiscard]] void* PopBlock(std::atomic<void*>& head) noexcept {
32 void* observed = head.load(std::memory_order_acquire);
33 for (;;) {
34 if (observed == nullptr) {
35 return nullptr;
36 }
37 void* const next = *static_cast<void**>(observed);
38 if (head.compare_exchange_weak(observed, next, std::memory_order_release,
39 std::memory_order_acquire)) {
40 return observed;
41 }
42 }
43}
44
45} // namespace
46
47namespace helios::mem {
48
50 : block_size_(std::max(options.block_size, sizeof(void*))),
51 initial_block_count_(options.block_count),
52 alignment_(options.alignment),
53 growth_(options.growth) {
54 HELIOS_ASSERT(block_size_ > 0, "block_size must be greater than zero!");
55 HELIOS_ASSERT(initial_block_count_ > 0,
56 "block_count must be greater than zero!");
57 HELIOS_ASSERT(IsPowerOfTwo(alignment_),
58 "alignment '{}' must be power of two!", alignment_);
59 HELIOS_ASSERT(alignment_ >= alignof(void*), "alignment '{}' must be >= '{}'!",
60 alignment_, alignof(void*));
61
62 block_size_ = AlignUp(block_size_, alignment_);
63
64 ChunkHeader* const initial_chunk =
65 CreateChunk(block_size_, initial_block_count_, alignment_);
66 HELIOS_VERIFY(initial_chunk != nullptr, "Failed to allocate pool chunk!");
67
68 chunks_.store(initial_chunk, std::memory_order_release);
69 total_blocks_.store(initial_chunk->block_count, std::memory_order_relaxed);
70 free_blocks_.store(initial_chunk->block_count, std::memory_order_relaxed);
71 PushChunkBlocks(*initial_chunk);
72}
73
74bool PoolAllocator::Owns(const void* ptr) const noexcept {
75 HELIOS_MEMORY_PROFILE_SCOPE_N("helios::mem::PoolAllocator::Owns");
76
77 if (ptr == nullptr) [[unlikely]] {
78 return false;
79 }
80
81 const auto addr = reinterpret_cast<uintptr_t>(ptr);
82 ChunkHeader* chunk = chunks_.load(std::memory_order_acquire);
83 while (chunk != nullptr) {
84 const auto begin = reinterpret_cast<uintptr_t>(chunk->buffer);
85 const auto end = begin + chunk->capacity;
86 if (addr >= begin && addr < end) {
87 return ((addr - begin) % block_size_) == 0;
88 }
89 chunk = chunk->next.load(std::memory_order_acquire);
90 }
91
92 return false;
93}
94
95void PoolAllocator::MoveFrom(PoolAllocator& other) noexcept {
96 block_size_ = std::exchange(other.block_size_, 0);
97 initial_block_count_ = std::exchange(other.initial_block_count_, 0);
98 alignment_ = std::exchange(other.alignment_, 0);
99 growth_ = std::exchange(other.growth_, {});
100 free_head_.store(
101 other.free_head_.exchange(nullptr, std::memory_order_acq_rel),
102 std::memory_order_release);
103 chunks_.store(other.chunks_.exchange(nullptr, std::memory_order_acq_rel),
104 std::memory_order_release);
105 grow_state_.store(
106 other.grow_state_.exchange(GrowState::kIdle, std::memory_order_acq_rel),
107 std::memory_order_release);
108 total_blocks_.store(
109 other.total_blocks_.exchange(0, std::memory_order_acq_rel),
110 std::memory_order_release);
111 free_blocks_.store(other.free_blocks_.exchange(0, std::memory_order_acq_rel),
112 std::memory_order_release);
113 peak_used_blocks_.store(
114 other.peak_used_blocks_.exchange(0, std::memory_order_acq_rel),
115 std::memory_order_release);
116 total_allocations_.store(
117 other.total_allocations_.exchange(0, std::memory_order_acq_rel),
118 std::memory_order_release);
119 total_deallocations_.store(
120 other.total_deallocations_.exchange(0, std::memory_order_acq_rel),
121 std::memory_order_release);
122}
123
124auto PoolAllocator::CreateChunk(size_t block_size, size_t block_count,
125 size_t alignment) noexcept -> ChunkHeader* {
126 constexpr size_t kHeaderAlignment = alignof(ChunkHeader);
127 const size_t chunk_alignment =
128 std::max({alignment, kHeaderAlignment, kMinAlignment});
129 const size_t header_size = AlignUp(sizeof(ChunkHeader), chunk_alignment);
130 const size_t payload = SaturatingMul(block_size, block_count);
131 const size_t total_size = SaturatingAdd(header_size, payload);
132
133 void* const raw = AlignedAlloc(chunk_alignment, total_size, false);
134 if (raw == nullptr) [[unlikely]] {
135 return nullptr;
136 }
137
138 HELIOS_MEMORY_PROFILE_ALLOC(raw, total_size, "PoolAllocator");
139
140 auto* const chunk = std::construct_at(static_cast<ChunkHeader*>(raw));
141 chunk->buffer = static_cast<std::byte*>(raw) + header_size;
142 chunk->capacity = payload;
143 chunk->block_count = block_count;
144 chunk->next.store(nullptr, std::memory_order_relaxed);
145 return chunk;
146}
147
148void PoolAllocator::FreeChunkChain(ChunkHeader* chunk) noexcept {
149 if (chunk == nullptr) [[unlikely]] {
150 return;
151 }
152
153 ChunkHeader* current = chunk;
154 while (current != nullptr) {
155 ChunkHeader* const next = current->next.load(std::memory_order_relaxed);
156 HELIOS_MEMORY_PROFILE_FREE(current, "PoolAllocator");
157 std::destroy_at(current);
158 AlignedFree(current, false);
159 current = next;
160 }
161}
162
163bool PoolAllocator::GrowIfNeeded() noexcept {
164 const size_t current_blocks = total_blocks_.load(std::memory_order_acquire);
165 const size_t desired_blocks =
166 growth_.NextCapacity(current_blocks, current_blocks + 1);
167 if (desired_blocks <= current_blocks) {
168 return false;
169 }
170
171 auto expected = GrowState::kIdle;
172 if (!grow_state_.compare_exchange_strong(expected, GrowState::kGrowing,
173 std::memory_order_acq_rel,
174 std::memory_order_acquire)) {
175 grow_state_.wait(GrowState::kGrowing, std::memory_order_acquire);
176 // Another allocator completed the coordinated attempt. Its blocks may
177 // already have been consumed, so the caller must retry the pop/grow loop.
178 return true;
179 }
180
181 const size_t chunk_blocks = desired_blocks - current_blocks;
182 ChunkHeader* const chunk = CreateChunk(block_size_, chunk_blocks, alignment_);
183 if (chunk == nullptr) {
184 grow_state_.store(GrowState::kIdle, std::memory_order_release);
185 grow_state_.notify_all();
186 return false;
187 }
188
189 ChunkHeader* observed = chunks_.load(std::memory_order_acquire);
190 do {
191 chunk->next.store(observed, std::memory_order_relaxed);
192 } while (!chunks_.compare_exchange_weak(
193 observed, chunk, std::memory_order_release, std::memory_order_acquire));
194
195 total_blocks_.fetch_add(chunk->block_count, std::memory_order_relaxed);
196 free_blocks_.fetch_add(chunk->block_count, std::memory_order_relaxed);
197 PushChunkBlocks(*chunk);
198
199 grow_state_.store(GrowState::kIdle, std::memory_order_release);
200 grow_state_.notify_all();
201 return true;
202}
203
204void PoolAllocator::PushChunkBlocks(ChunkHeader& chunk) noexcept {
205 auto* current = static_cast<std::byte*>(chunk.buffer);
206 for (size_t i = 0; i < chunk.block_count; ++i) {
207 PushBlock(free_head_, current);
208 current += block_size_;
209 }
210}
211
212void PoolAllocator::RebuildFreeList() noexcept {
213 free_head_.store(nullptr, std::memory_order_release);
214 ChunkHeader* chunk = chunks_.load(std::memory_order_acquire);
215 size_t total = 0;
216
217 while (chunk != nullptr) {
218 PushChunkBlocks(*chunk);
219 total += chunk->block_count;
220 chunk = chunk->next.load(std::memory_order_acquire);
221 }
222
223 total_blocks_.store(total, std::memory_order_release);
224 free_blocks_.store(total, std::memory_order_release);
225}
226
227void* PoolAllocator::do_allocate(size_t bytes, size_t alignment) {
228 HELIOS_MEMORY_PROFILE_SCOPE_N("helios::mem::PoolAllocator::do_allocate");
229 HELIOS_MEMORY_PROFILE_ZONE_VALUE(bytes);
230
231 if (bytes == 0) [[unlikely]] {
232 return nullptr;
233 }
234
235 HELIOS_VERIFY(IsPowerOfTwo(alignment),
236 "alignment must be a power of two, got {}!", alignment);
237
238 HELIOS_VERIFY(alignment <= alignment_,
239 "Requested alignment ({}) exceeds pool alignment ({})!",
240 alignment, alignment_);
241
242 HELIOS_VERIFY(bytes <= block_size_,
243 "Requested size ({}) exceeds pool block size ({})!", bytes,
244 block_size_);
245
246 void* block = nullptr;
247 while ((block = PopBlock(free_head_)) == nullptr) {
248 HELIOS_VERIFY(GrowIfNeeded(), "Pool exhausted and growth failed!");
249 }
250
251 const size_t free_now =
252 free_blocks_.fetch_sub(1, std::memory_order_relaxed) - 1;
253 total_allocations_.fetch_add(1, std::memory_order_relaxed);
254
255 const size_t total = total_blocks_.load(std::memory_order_relaxed);
256 const size_t used = total - free_now;
257 details::AccumulatePeak(peak_used_blocks_, used);
258
259 return block;
260}
261
262void PoolAllocator::do_deallocate(void* ptr, [[maybe_unused]] size_t bytes,
263 size_t /*alignment*/) {
264 HELIOS_MEMORY_PROFILE_SCOPE_N("helios::mem::PoolAllocator::do_deallocate");
265 HELIOS_MEMORY_PROFILE_ZONE_VALUE(bytes);
266
267 if (ptr == nullptr) [[unlikely]] {
268 return;
269 }
270
271 HELIOS_ASSERT(Owns(ptr), "ptr does not belong to pool allocator!");
272
273 PushBlock(free_head_, ptr);
274 free_blocks_.fetch_add(1, std::memory_order_relaxed);
275 total_deallocations_.fetch_add(1, std::memory_order_relaxed);
276}
277
278} // namespace helios::mem
#define HELIOS_ASSERT(condition,...)
Assertion macro that aborts execution in debug builds.
Definition assert.hpp:259
#define HELIOS_VERIFY(condition,...)
Verify macro that always checks the condition.
Definition assert.hpp:323
Lock-free PMR pool allocator for fixed-size blocks.
PoolAllocator(PoolAllocatorOptions options) noexcept
Constructs pool allocator from options.
bool Owns(const void *ptr) const noexcept
Checks pointer ownership.
void PushBlock(std::atomic< void * > &head, void *block) noexcept
void * PopBlock(std::atomic< void * > &head) noexcept
void * PopBlock(std::atomic< void * > &head) noexcept
void PushBlock(std::atomic< void * > &head, void *block) noexcept
constexpr bool IsPowerOfTwo(size_t value) noexcept
Checks if a value is a power of two.
Definition common.hpp:215
constexpr size_t AlignUp(size_t value, size_t alignment) noexcept
Aligns value up to alignment.
Definition common.hpp:226
constexpr size_t kMinAlignment
Minimum alignment used by memory resources.
Definition common.hpp:17
constexpr size_t SaturatingMul(size_t lhs, size_t rhs) noexcept
Saturating multiply for size_t.
Definition common.hpp:39
void * AlignedAlloc(size_t alignment, size_t size, bool enable_profile=true) noexcept
Allocates memory with the specified alignment.
void AlignedFree(void *ptr, bool enable_profile=true) noexcept
Frees memory allocated with AlignedAlloc.
constexpr size_t SaturatingAdd(size_t lhs, size_t rhs) noexcept
Saturating add for size_t.
Definition common.hpp:25
Configuration for PoolAllocator.