Helios Engine
A modular ECS based data-oriented C++23 game engine framework
Loading...
Searching...
No Matches
fixed_free_list_allocator.cpp
Go to the documentation of this file.
1#include <pch.hpp>
2
4
5#include <helios/assert.hpp>
8#include <helios/memory/details/profile.hpp>
9
10#include <algorithm>
11#include <atomic>
12#include <cstddef>
13#include <limits>
14#include <mutex>
15#include <new>
16#include <string_view>
17#include <utility>
18
19namespace helios::mem {
20
21void FixedFreeListAllocator::Initialize() noexcept {
22 free_list_ = std::launder(reinterpret_cast<FreeBlock*>(buffer_));
23 free_list_->size = capacity_;
24 free_list_->next = nullptr;
25 free_block_count_.store(1, std::memory_order_relaxed);
26 used_memory_.store(0, std::memory_order_relaxed);
27 allocation_count_.store(0, std::memory_order_relaxed);
28 alignment_waste_.store(0, std::memory_order_relaxed);
29}
30
31void FixedFreeListAllocator::MoveFrom(FixedFreeListAllocator& other) noexcept {
32 capacity_ = std::exchange(other.capacity_, 0);
33 buffer_ = std::exchange(other.buffer_, nullptr);
34 free_list_ = std::exchange(other.free_list_, nullptr);
35 free_block_count_.store(
36 other.free_block_count_.exchange(0, std::memory_order_acq_rel),
37 std::memory_order_release);
38 used_memory_.store(other.used_memory_.exchange(0, std::memory_order_acq_rel),
39 std::memory_order_release);
40 peak_usage_.store(other.peak_usage_.exchange(0, std::memory_order_acq_rel),
41 std::memory_order_release);
42 allocation_count_.store(
43 other.allocation_count_.exchange(0, std::memory_order_acq_rel),
44 std::memory_order_release);
45 total_allocations_.store(
46 other.total_allocations_.exchange(0, std::memory_order_acq_rel),
47 std::memory_order_release);
48 total_deallocations_.store(
49 other.total_deallocations_.exchange(0, std::memory_order_acq_rel),
50 std::memory_order_release);
51 alignment_waste_.store(
52 other.alignment_waste_.exchange(0, std::memory_order_acq_rel),
53 std::memory_order_release);
54}
55
56void FixedFreeListAllocator::InsertAndCoalesce(FreeBlock* block) noexcept {
57 FreeBlock* previous = nullptr;
58 FreeBlock* current = free_list_;
59 while (current != nullptr && current < block) {
60 previous = current;
61 current = current->next;
62 }
63
64 block->next = current;
65
66 if (previous == nullptr) {
67 free_list_ = block;
68 } else {
69 previous->next = block;
70 }
71
72 free_block_count_.fetch_add(1, std::memory_order_relaxed);
73 if (current != nullptr && reinterpret_cast<std::byte*>(block) + block->size ==
74 reinterpret_cast<std::byte*>(current)) {
75 block->size += current->size;
76 block->next = current->next;
77 free_block_count_.fetch_sub(1, std::memory_order_relaxed);
78 }
79
80 if (previous != nullptr &&
81 reinterpret_cast<std::byte*>(previous) + previous->size ==
82 reinterpret_cast<std::byte*>(block)) {
83 previous->size += block->size;
84 previous->next = block->next;
85 free_block_count_.fetch_sub(1, std::memory_order_relaxed);
86 }
87}
88
89void FixedFreeListAllocator::ReleaseUnlocked() noexcept {
90 if (buffer_ != nullptr) {
91 HELIOS_MEMORY_PROFILE_FREE(buffer_, "FixedFreeListAllocator");
92 AlignedFree(buffer_, false);
93 buffer_ = nullptr;
94 free_list_ = nullptr;
95 }
96}
97
98void FixedFreeListAllocator::Release() noexcept {
99 const std::scoped_lock lock(mutex_);
100 ReleaseUnlocked();
101}
102
103void* FixedFreeListAllocator::do_allocate(size_t bytes, size_t alignment) {
104 HELIOS_MEMORY_PROFILE_SCOPE_N(
105 "helios::mem::FixedFreeListAllocator::do_allocate");
106 HELIOS_MEMORY_PROFILE_ZONE_VALUE(bytes);
107
108 if (bytes == 0) [[unlikely]] {
109 return nullptr;
110 }
111
112 HELIOS_VERIFY(IsPowerOfTwo(alignment),
113 "alignment must be a power of two, got {}!", alignment);
114
115 const std::scoped_lock lock(mutex_);
116 HELIOS_MEMORY_PROFILE_LOCK_MARK(mutex_);
117
118 const size_t effective_alignment = std::max(alignment, kMinAlignment);
119 FreeBlock* best = nullptr;
120 FreeBlock* best_previous = nullptr;
121 size_t best_padding = 0;
122 size_t best_total = std::numeric_limits<size_t>::max();
123 FreeBlock* previous = nullptr;
124 for (FreeBlock* block = free_list_; block != nullptr;
125 previous = block, block = block->next) {
126 const size_t padding = CalculatePaddingWithHeader(
127 block, effective_alignment, sizeof(AllocationHeader));
128 const size_t total = SaturatingAdd(padding, bytes);
129 if (block->size >= total && total < best_total) {
130 best = block;
131 best_previous = previous;
132 best_padding = padding;
133 best_total = total;
134 }
135 }
136
137 HELIOS_VERIFY(best != nullptr, "Fixed free-list allocator exhausted!");
138
139 if (best_previous != nullptr) {
140 best_previous->next = best->next;
141 } else {
142 free_list_ = best->next;
143 }
144
145 free_block_count_.fetch_sub(1, std::memory_order_relaxed);
146 size_t remaining = best->size - best_total;
147 auto* split_address = reinterpret_cast<std::byte*>(best) + best_total;
148 const size_t split_padding =
149 CalculatePadding(split_address, alignof(FreeBlock));
150
151 if (remaining >= split_padding + sizeof(FreeBlock) + 1) {
152 best_total += split_padding;
153 remaining -= split_padding;
154 split_address += split_padding;
155 auto* const split =
156 std::launder(reinterpret_cast<FreeBlock*>(split_address));
157 split->size = remaining;
158 split->next = nullptr;
159 InsertAndCoalesce(split);
160 } else {
161 best_total = best->size;
162 }
163
164 auto* const result =
165 std::launder(reinterpret_cast<std::byte*>(best) + best_padding);
166 auto* const header =
167 std::launder(reinterpret_cast<AllocationHeader*>(result) - 1);
168 header->size = best_total;
169 header->padding = best_padding;
170
171 const size_t used =
172 used_memory_.fetch_add(best_total, std::memory_order_relaxed) +
173 best_total;
174 size_t peak = peak_usage_.load(std::memory_order_relaxed);
175 while (used > peak && !peak_usage_.compare_exchange_weak(
176 peak, used, std::memory_order_relaxed)) {
177 }
178
179 allocation_count_.fetch_add(1, std::memory_order_relaxed);
180 total_allocations_.fetch_add(1, std::memory_order_relaxed);
181 alignment_waste_.fetch_add(best_padding - sizeof(AllocationHeader),
182 std::memory_order_relaxed);
183 return result;
184}
185
186void FixedFreeListAllocator::do_deallocate(void* ptr, size_t /*bytes*/,
187 size_t /*alignment*/) {
188 HELIOS_MEMORY_PROFILE_SCOPE_N(
189 "helios::mem::FixedFreeListAllocator::do_deallocate");
190
191 if (ptr == nullptr) [[unlikely]] {
192 return;
193 }
194
195 HELIOS_ASSERT(Owns(ptr), "ptr does not belong to fixed free-list!");
196 const std::scoped_lock lock(mutex_);
197 HELIOS_MEMORY_PROFILE_LOCK_MARK(mutex_);
198
199 auto* const header =
200 std::launder(reinterpret_cast<AllocationHeader*>(ptr) - 1);
201 auto* const block = std::launder(reinterpret_cast<FreeBlock*>(
202 static_cast<std::byte*>(ptr) - header->padding));
203 block->size = header->size;
204 block->next = nullptr;
205
206 used_memory_.fetch_sub(header->size, std::memory_order_relaxed);
207 allocation_count_.fetch_sub(1, std::memory_order_relaxed);
208 total_deallocations_.fetch_add(1, std::memory_order_relaxed);
209
210 InsertAndCoalesce(block);
211}
212
213} // 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
Fixed-capacity best-fit allocator with coalescing.
bool Owns(const void *ptr) const noexcept
Checks whether pointer belongs to this allocator's backing buffer.
constexpr bool IsPowerOfTwo(size_t value) noexcept
Checks if a value is a power of two.
Definition common.hpp:215
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 AlignedFree(void *ptr, bool enable_profile=true) noexcept
Frees memory allocated with AlignedAlloc.
size_t CalculatePadding(const void *ptr, size_t alignment) noexcept
Computes padding required for pointer alignment.
Definition common.hpp:274
constexpr size_t SaturatingAdd(size_t lhs, size_t rhs) noexcept
Saturating add for size_t.
Definition common.hpp:25