Helios Engine
A modular ECS based data-oriented C++23 game engine framework
Loading...
Searching...
No Matches
free_list_allocator.cpp
Go to the documentation of this file.
1#include <pch.hpp>
2
4
5#include <details/accumulate_peak.hpp>
6#include <details/deferred_region_ebr.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 <mutex>
17#include <string_view>
18#include <utility>
19
20namespace {
21
23 size_t size = 0;
24 size_t padding = 0;
25};
26
27} // namespace
28
29namespace helios::mem {
30
31void FreeListAllocator::Reset() noexcept {
32 HELIOS_MEMORY_PROFILE_SCOPE_N("helios::mem::FreeListAllocator::Reset");
33
34 const std::scoped_lock lock(mutex_);
35 free_list_ = nullptr;
36 free_block_count_.store(0, std::memory_order_relaxed);
37
38 RegionHeader* region = regions_.load(std::memory_order_acquire);
39 while (region != nullptr) {
40 InitializeRegionLocked(*region);
41 region = region->next.load(std::memory_order_acquire);
42 }
43
44 used_memory_.store(0, std::memory_order_release);
45 peak_usage_.store(0, std::memory_order_release);
46 allocation_count_.store(0, std::memory_order_release);
47 total_allocations_.store(0, std::memory_order_release);
48 total_deallocations_.store(0, std::memory_order_release);
49 alignment_waste_.store(0, std::memory_order_release);
50}
51
52bool FreeListAllocator::Owns(const void* ptr) const noexcept {
53 HELIOS_MEMORY_PROFILE_SCOPE_N("helios::mem::FreeListAllocator::Owns");
54
55 if (ptr == nullptr) [[unlikely]] {
56 return false;
57 }
58
59 const auto addr = reinterpret_cast<uintptr_t>(ptr);
60 const details::DeferredRegionEpochGuard guard;
61 RegionHeader* region = regions_.load(std::memory_order_acquire);
62 while (region != nullptr) {
63 const auto begin = reinterpret_cast<uintptr_t>(region->buffer);
64 const auto end = begin + region->capacity;
65 if (addr >= begin && addr < end) {
66 return true;
67 }
68 region = region->next.load(std::memory_order_acquire);
69 }
70
71 return false;
72}
73
74void FreeListAllocator::MoveFrom(FreeListAllocator& other) noexcept {
75 free_list_ = std::exchange(other.free_list_, nullptr);
76 regions_.store(other.regions_.exchange(nullptr, std::memory_order_acq_rel),
77 std::memory_order_release);
78 grow_state_.store(
79 other.grow_state_.exchange(GrowState::kIdle, std::memory_order_acq_rel),
80 std::memory_order_release);
81 initial_capacity_ = std::exchange(other.initial_capacity_, 0);
82 growth_ = std::exchange(other.growth_, {});
83 capacity_.store(other.capacity_.exchange(0, std::memory_order_acq_rel),
84 std::memory_order_release);
85 used_memory_.store(other.used_memory_.exchange(0, std::memory_order_acq_rel),
86 std::memory_order_release);
87 peak_usage_.store(other.peak_usage_.exchange(0, std::memory_order_acq_rel),
88 std::memory_order_release);
89 free_block_count_.store(
90 other.free_block_count_.exchange(0, std::memory_order_acq_rel),
91 std::memory_order_release);
92 allocation_count_.store(
93 other.allocation_count_.exchange(0, std::memory_order_acq_rel),
94 std::memory_order_release);
95 total_allocations_.store(
96 other.total_allocations_.exchange(0, std::memory_order_acq_rel),
97 std::memory_order_release);
98 total_deallocations_.store(
99 other.total_deallocations_.exchange(0, std::memory_order_acq_rel),
100 std::memory_order_release);
101 alignment_waste_.store(
102 other.alignment_waste_.exchange(0, std::memory_order_acq_rel),
103 std::memory_order_release);
104}
105
106void FreeListAllocator::ReleaseRegions() noexcept {
107 FreeRegions(regions_.load(std::memory_order_acquire));
108 details::FlushDeferredRegions();
109}
110
111size_t FreeListAllocator::FreeMemory() const noexcept {
112 const size_t cap = capacity_.load(std::memory_order_relaxed);
113 const size_t used = used_memory_.load(std::memory_order_relaxed);
114 return cap >= used ? cap - used : 0;
115}
116
117auto FreeListAllocator::CreateRegion(size_t capacity) noexcept
118 -> RegionHeader* {
119 constexpr size_t kAlign =
120 std::max({alignof(RegionHeader), kDefaultAlignment, kMinAlignment});
121 const size_t header_size = AlignUp(sizeof(RegionHeader), kAlign);
122 const size_t total_size = SaturatingAdd(header_size, capacity);
123
124 void* const raw = AlignedAlloc(kAlign, total_size, false);
125 if (raw == nullptr) [[unlikely]] {
126 return nullptr;
127 }
128
129 HELIOS_MEMORY_PROFILE_ALLOC(raw, total_size, "FreeListAllocator");
130
131 auto* const region = std::construct_at(static_cast<RegionHeader*>(raw));
132 region->buffer = static_cast<std::byte*>(raw) + header_size;
133 region->capacity = capacity;
134 region->next.store(nullptr, std::memory_order_relaxed);
135 return region;
136}
137
138void FreeListAllocator::FreeRegions(RegionHeader* region) noexcept {
139 if (region == nullptr) [[unlikely]] {
140 return;
141 }
142
143 while (region != nullptr) {
144 RegionHeader* const next = region->next.load(std::memory_order_relaxed);
145 details::RetireRegionAllocation(region);
146 region = next;
147 }
148}
149
150void* FreeListAllocator::AllocateLocked(size_t bytes,
151 size_t alignment) noexcept {
152 constexpr size_t kHeaderSize = sizeof(AllocationHeader);
153 const size_t effective_alignment = std::max(alignment, kMinAlignment);
154 const size_t storage_alignment =
155 std::max(effective_alignment, alignof(AllocationHeader));
156
157 FreeBlockHeader* best = nullptr;
158 FreeBlockHeader* best_prev = nullptr;
159 size_t best_padding = 0;
160 size_t best_total = std::numeric_limits<size_t>::max();
161
162 FreeBlockHeader* current = free_list_;
163 FreeBlockHeader* prev = nullptr;
164
165 while (current != nullptr) {
166 auto* const block_start = reinterpret_cast<std::byte*>(current);
167 const size_t padding =
168 CalculatePaddingWithHeader(block_start, storage_alignment, kHeaderSize);
169 const size_t total = SaturatingAdd(padding, bytes);
170
171 if (current->size >= total && total < best_total) {
172 best = current;
173 best_prev = prev;
174 best_padding = padding;
175 best_total = total;
176 if (current->size == total) {
177 break;
178 }
179 }
180
181 prev = current;
182 current = current->next;
183 }
184
185 if (best == nullptr) [[unlikely]] {
186 return nullptr;
187 }
188
189 if (best_prev != nullptr) {
190 best_prev->next = best->next;
191 } else {
192 free_list_ = best->next;
193 }
194 free_block_count_.fetch_sub(1, std::memory_order_relaxed);
195
196 const size_t remaining = best->size - best_total;
197 constexpr size_t kMinSplit =
198 sizeof(FreeBlockHeader) + alignof(FreeBlockHeader);
199 if (remaining >= kMinSplit) {
200 auto* const split_raw = reinterpret_cast<std::byte*>(best) + best_total;
201 const size_t split_pad =
202 CalculatePadding(split_raw, alignof(FreeBlockHeader));
203 auto* const split_block =
204 std::launder(reinterpret_cast<FreeBlockHeader*>(split_raw + split_pad));
205 const size_t split_size =
206 remaining >= split_pad ? remaining - split_pad : 0;
207 if (split_size >= sizeof(FreeBlockHeader) + 1) {
208 split_block->size = split_size;
209 split_block->next = nullptr;
210 InsertAndCoalesceLocked(split_block);
211 }
212 }
213
214 auto* const user_ptr = reinterpret_cast<std::byte*>(best) + best_padding;
215 auto* const header =
216 std::launder(reinterpret_cast<AllocationHeader*>(user_ptr - kHeaderSize));
217 header->size = best_total;
218 header->padding = best_padding;
219 alignment_waste_.fetch_add(
220 best_padding > kHeaderSize ? best_padding - kHeaderSize : 0,
221 std::memory_order_relaxed);
222 return user_ptr;
223}
224
225void FreeListAllocator::DeallocateLocked(void* ptr) noexcept {
226 auto* const header = std::launder(reinterpret_cast<AllocationHeader*>(
227 static_cast<std::byte*>(ptr) - sizeof(AllocationHeader)));
228 auto* const block_begin = static_cast<std::byte*>(ptr) - header->padding;
229 auto* const free_block =
230 std::launder(reinterpret_cast<FreeBlockHeader*>(block_begin));
231 free_block->size = header->size;
232 free_block->next = nullptr;
233 InsertAndCoalesceLocked(free_block);
234}
235
236bool FreeListAllocator::EnsureCapacity(size_t min_capacity) noexcept {
237 const size_t current_capacity = capacity_.load(std::memory_order_relaxed);
238 const size_t next_capacity = growth_.NextCapacity(
239 current_capacity, SaturatingAdd(current_capacity, min_capacity));
240 if (next_capacity <= current_capacity) {
241 return false;
242 }
243
244 auto expected = GrowState::kIdle;
245 if (!grow_state_.compare_exchange_strong(expected, GrowState::kGrowing,
246 std::memory_order_acq_rel,
247 std::memory_order_acquire)) {
248 grow_state_.wait(GrowState::kGrowing, std::memory_order_acquire);
249 return true;
250 }
251
252 const size_t region_capacity = next_capacity - current_capacity;
253 RegionHeader* const region = CreateRegion(region_capacity);
254 if (region == nullptr) [[unlikely]] {
255 grow_state_.store(GrowState::kIdle, std::memory_order_release);
256 grow_state_.notify_all();
257 return false;
258 }
259
260 {
261 const std::scoped_lock lock(mutex_);
262 HELIOS_MEMORY_PROFILE_LOCK_MARK(mutex_);
263 RegionHeader* observed = regions_.load(std::memory_order_acquire);
264 do {
265 region->next.store(observed, std::memory_order_relaxed);
266 } while (!regions_.compare_exchange_weak(observed, region,
267 std::memory_order_release,
268 std::memory_order_acquire));
269
270 capacity_.fetch_add(region_capacity, std::memory_order_relaxed);
271 InitializeRegionLocked(*region);
272 }
273
274 grow_state_.store(GrowState::kIdle, std::memory_order_release);
275 grow_state_.notify_all();
276 return true;
277}
278
279void FreeListAllocator::InsertAndCoalesceLocked(
280 FreeBlockHeader* block) noexcept {
281 const auto block_addr = reinterpret_cast<uintptr_t>(block);
282 FreeBlockHeader* prev = nullptr;
283 FreeBlockHeader* current = free_list_;
284
285 while (current != nullptr &&
286 reinterpret_cast<uintptr_t>(current) < block_addr) {
287 prev = current;
288 current = current->next;
289 }
290
291 block->next = current;
292 if (prev != nullptr) {
293 prev->next = block;
294 } else {
295 free_list_ = block;
296 }
297 free_block_count_.fetch_add(1, std::memory_order_relaxed);
298
299 if (current != nullptr) {
300 auto* const block_end = reinterpret_cast<std::byte*>(block) + block->size;
301 if (block_end == reinterpret_cast<std::byte*>(current)) {
302 block->size += current->size;
303 block->next = current->next;
304 free_block_count_.fetch_sub(1, std::memory_order_relaxed);
305 }
306 }
307
308 if (prev != nullptr) {
309 auto* const prev_end = reinterpret_cast<std::byte*>(prev) + prev->size;
310 if (prev_end == reinterpret_cast<std::byte*>(block)) {
311 prev->size += block->size;
312 prev->next = block->next;
313 free_block_count_.fetch_sub(1, std::memory_order_relaxed);
314 }
315 }
316}
317
318void FreeListAllocator::InitializeRegionLocked(RegionHeader& region) noexcept {
319 auto* const block = static_cast<FreeBlockHeader*>(region.buffer);
320 block->size = region.capacity;
321 block->next = nullptr;
322 InsertAndCoalesceLocked(block);
323}
324
325void* FreeListAllocator::do_allocate(size_t bytes, size_t alignment) {
326 HELIOS_MEMORY_PROFILE_SCOPE_N("helios::mem::FreeListAllocator::do_allocate");
327 HELIOS_MEMORY_PROFILE_ZONE_VALUE(bytes);
328
329 if (bytes == 0) [[unlikely]] {
330 return nullptr;
331 }
332
333 HELIOS_VERIFY(IsPowerOfTwo(alignment),
334 "alignment must be a power of two, got {}!", alignment);
335
336 void* result = nullptr;
337 while (result == nullptr) {
338 {
339 const std::scoped_lock lock(mutex_);
340 HELIOS_MEMORY_PROFILE_LOCK_MARK(mutex_);
341 result = AllocateLocked(bytes, alignment);
342 if (result != nullptr) {
343 break;
344 }
345 }
346
347 HELIOS_VERIFY(EnsureCapacity(SaturatingAdd(bytes, alignment)),
348 "Free-list allocator out of memory and cannot grow!");
349 }
350
351 const auto* header = std::launder(reinterpret_cast<const AllocationHeader*>(
352 static_cast<const std::byte*>(result) - sizeof(AllocationHeader)));
353 const size_t consumed = header->size;
354
355 const size_t used =
356 used_memory_.fetch_add(consumed, std::memory_order_relaxed) + consumed;
357 details::AccumulatePeak(peak_usage_, used);
358 allocation_count_.fetch_add(1, std::memory_order_relaxed);
359 total_allocations_.fetch_add(1, std::memory_order_relaxed);
360
361 return result;
362}
363
364void FreeListAllocator::do_deallocate(void* ptr, [[maybe_unused]] size_t bytes,
365 size_t /*alignment*/) {
366 HELIOS_MEMORY_PROFILE_SCOPE_N(
367 "helios::mem::FreeListAllocator::do_deallocate");
368 HELIOS_MEMORY_PROFILE_ZONE_VALUE(bytes);
369
370 if (ptr == nullptr) [[unlikely]] {
371 return;
372 }
373
374 HELIOS_ASSERT(Owns(ptr), "ptr does not belong to free-list allocator!");
375
376 size_t consumed = 0;
377 {
378 const std::scoped_lock lock(mutex_);
379 HELIOS_MEMORY_PROFILE_LOCK_MARK(mutex_);
380 const auto* header = std::launder(reinterpret_cast<const AllocationHeader*>(
381 static_cast<const std::byte*>(ptr) - sizeof(AllocationHeader)));
382 consumed = header->size;
383 DeallocateLocked(ptr);
384 }
385
386 used_memory_.fetch_sub(consumed, std::memory_order_relaxed);
387 allocation_count_.fetch_sub(1, std::memory_order_relaxed);
388 total_deallocations_.fetch_add(1, std::memory_order_relaxed);
389}
390
391} // 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
General-purpose PMR free-list allocator.
size_t FreeMemory() const noexcept
Returns free memory bytes.
void Reset() noexcept
Resets allocator to initial state.
bool Owns(const void *ptr) const noexcept
Checks whether pointer belongs to allocator storage.
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
size_t CalculatePaddingWithHeader(const void *ptr, size_t alignment, size_t header_size) noexcept
Computes padding for aligned storage with a prepended header.
Definition common.hpp:288
void * AlignedAlloc(size_t alignment, size_t size, bool enable_profile=true) noexcept
Allocates memory with the specified alignment.
size_t CalculatePadding(const void *ptr, size_t alignment) noexcept
Computes padding required for pointer alignment.
Definition common.hpp:274
constexpr size_t kDefaultAlignment
Default alignment used by memory resources.
Definition common.hpp:14
constexpr size_t SaturatingAdd(size_t lhs, size_t rhs) noexcept
Saturating add for size_t.
Definition common.hpp:25