Helios Engine
A modular ECS based data-oriented C++23 game engine framework
Loading...
Searching...
No Matches
stack_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 helios::mem {
19
20void StackAllocator::Reset() noexcept {
21 HELIOS_MEMORY_PROFILE_SCOPE_N("helios::mem::StackAllocator::Reset");
22
23 Block* const head = head_.load(std::memory_order_acquire);
24 if (head == nullptr) {
25 return;
26 }
27
28 FreeChain(head);
29 Block* const fresh = CreateBlock(initial_capacity_);
30 HELIOS_VERIFY(fresh != nullptr, "Failed to allocate block during reset!");
31
32 head_.store(fresh, std::memory_order_release);
33 total_capacity_.store(initial_capacity_, std::memory_order_release);
34 total_allocated_.store(0, std::memory_order_release);
35 allocation_count_.store(0, std::memory_order_release);
36 total_allocations_.store(0, std::memory_order_release);
37 total_deallocations_.store(0, std::memory_order_release);
38 alignment_waste_.store(0, std::memory_order_release);
39 block_count_.store(1, std::memory_order_release);
40}
41
43 HELIOS_MEMORY_PROFILE_SCOPE_N("helios::mem::StackAllocator::RewindToMarker");
44 HELIOS_MEMORY_PROFILE_ZONE_VALUE(marker.offset);
45
46 HELIOS_ASSERT(marker.block != nullptr, "marker block cannot be null");
47 auto* const target = static_cast<Block*>(marker.block);
48
49 Block* current = head_.load(std::memory_order_acquire);
50 while (current != nullptr && current != target) {
51 Block* const next = current->next.load(std::memory_order_relaxed);
52 const size_t capacity = current->capacity;
53 HELIOS_MEMORY_PROFILE_FREE(current, "StackAllocator");
54 std::destroy_at(current);
55 AlignedFree(current, false);
56 total_capacity_.fetch_sub(capacity, std::memory_order_relaxed);
57 block_count_.fetch_sub(1, std::memory_order_relaxed);
58 current = next;
59 }
60
61 HELIOS_ASSERT(current == target, "Marker block not found in stack chain!");
62
63 target->offset.store(marker.offset, std::memory_order_release);
64 head_.store(target, std::memory_order_release);
65
66 allocation_count_.store(0, std::memory_order_relaxed);
67 total_allocated_.store(marker.offset, std::memory_order_relaxed);
68}
69
70void StackAllocator::MoveFrom(StackAllocator& other) noexcept {
71 head_.store(other.head_.exchange(nullptr, std::memory_order_acq_rel),
72 std::memory_order_release);
73 grow_state_.store(
74 other.grow_state_.exchange(GrowState::kIdle, std::memory_order_acq_rel),
75 std::memory_order_release);
76 initial_capacity_ = std::exchange(other.initial_capacity_, 0);
77 growth_ = std::exchange(other.growth_, {});
78 total_capacity_.store(
79 other.total_capacity_.exchange(0, std::memory_order_acq_rel),
80 std::memory_order_release);
81 total_allocated_.store(
82 other.total_allocated_.exchange(0, std::memory_order_acq_rel),
83 std::memory_order_release);
84 peak_usage_.store(other.peak_usage_.exchange(0, std::memory_order_acq_rel),
85 std::memory_order_release);
86 allocation_count_.store(
87 other.allocation_count_.exchange(0, std::memory_order_acq_rel),
88 std::memory_order_release);
89 total_allocations_.store(
90 other.total_allocations_.exchange(0, std::memory_order_acq_rel),
91 std::memory_order_release);
92 total_deallocations_.store(
93 other.total_deallocations_.exchange(0, std::memory_order_acq_rel),
94 std::memory_order_release);
95 alignment_waste_.store(
96 other.alignment_waste_.exchange(0, std::memory_order_acq_rel),
97 std::memory_order_release);
98 block_count_.store(other.block_count_.exchange(0, std::memory_order_acq_rel),
99 std::memory_order_release);
100}
101
102auto StackAllocator::CreateBlock(size_t capacity) noexcept -> Block* {
103 constexpr size_t kHeader = AlignUp(sizeof(Block), kDefaultAlignment);
104 const size_t total_size = SaturatingAdd(kHeader, capacity);
105 void* const raw = AlignedAlloc(kDefaultAlignment, total_size, false);
106 if (raw == nullptr) [[unlikely]] {
107 return nullptr;
108 }
109
110 HELIOS_MEMORY_PROFILE_ALLOC(raw, total_size, "StackAllocator");
111
112 auto* const block = std::construct_at(static_cast<Block*>(raw));
113 block->buffer = static_cast<std::byte*>(raw) + kHeader;
114 block->capacity = capacity;
115 block->offset.store(0, std::memory_order_relaxed);
116 block->next.store(nullptr, std::memory_order_relaxed);
117 return block;
118}
119
120void StackAllocator::FreeChain(Block* head) noexcept {
121 if (head == nullptr) [[unlikely]] {
122 return;
123 }
124
125 Block* current = head;
126 while (current != nullptr) {
127 Block* const next = current->next.load(std::memory_order_relaxed);
128 HELIOS_MEMORY_PROFILE_FREE(current, "StackAllocator");
129 std::destroy_at(current);
130 AlignedFree(current, false);
131 current = next;
132 }
133}
134
135auto StackAllocator::TryReserve(Block& block, size_t size,
136 size_t alignment) noexcept -> Reservation {
137 constexpr size_t kHeaderSize = sizeof(AllocationHeader);
138 const size_t header_alignment =
139 std::max(alignment, alignof(AllocationHeader));
140
141 size_t current_offset = block.offset.load(std::memory_order_acquire);
142 for (;;) {
143 auto* const start = static_cast<std::byte*>(block.buffer) + current_offset;
144 const size_t padding =
145 CalculatePaddingWithHeader(start, header_alignment, kHeaderSize);
146 const size_t consumed = SaturatingAdd(padding, size);
147 const size_t end_offset = SaturatingAdd(current_offset, consumed);
148
149 if (end_offset > block.capacity) {
150 return {};
151 }
152
153 if (block.offset.compare_exchange_weak(current_offset, end_offset,
154 std::memory_order_release,
155 std::memory_order_acquire)) {
156 auto* const user_ptr = start + padding;
157 auto* const header_ptr = std::launder(
158 reinterpret_cast<AllocationHeader*>(user_ptr - kHeaderSize));
159 header_ptr->previous_offset = current_offset;
160 header_ptr->total_size = consumed;
161
162 return {
163 .ptr = user_ptr,
164 .allocation_size = size,
165 .consumed = consumed,
166 .previous_offset = current_offset,
167 };
168 }
169 }
170}
171
172bool StackAllocator::EnsureCapacity(size_t min_capacity) noexcept {
173 Block* observed_head = head_.load(std::memory_order_acquire);
174 const size_t current_capacity =
175 observed_head != nullptr ? observed_head->capacity : initial_capacity_;
176 const size_t desired_capacity =
177 growth_.NextCapacity(current_capacity, min_capacity);
178 if (desired_capacity < min_capacity || desired_capacity == 0) {
179 return false;
180 }
181
182 auto expected = GrowState::kIdle;
183 if (!grow_state_.compare_exchange_strong(expected, GrowState::kGrowing,
184 std::memory_order_acq_rel,
185 std::memory_order_acquire)) {
186 grow_state_.wait(GrowState::kGrowing, std::memory_order_acquire);
187 return true;
188 }
189
190 Block* const new_block = CreateBlock(desired_capacity);
191 if (new_block == nullptr) {
192 grow_state_.store(GrowState::kIdle, std::memory_order_release);
193 grow_state_.notify_all();
194 return false;
195 }
196
197 PublishBlock(new_block);
198 grow_state_.store(GrowState::kIdle, std::memory_order_release);
199 grow_state_.notify_all();
200 return true;
201}
202
203void StackAllocator::PublishBlock(Block* block) noexcept {
204 Block* expected_head = head_.load(std::memory_order_acquire);
205 do {
206 block->next.store(expected_head, std::memory_order_relaxed);
207 } while (!head_.compare_exchange_weak(expected_head, block,
208 std::memory_order_release,
209 std::memory_order_acquire));
210
211 total_capacity_.fetch_add(block->capacity, std::memory_order_relaxed);
212 block_count_.fetch_add(1, std::memory_order_relaxed);
213}
214
215void* StackAllocator::do_allocate(size_t bytes, size_t alignment) {
216 HELIOS_MEMORY_PROFILE_SCOPE_N("helios::mem::StackAllocator::do_allocate");
217 HELIOS_MEMORY_PROFILE_ZONE_VALUE(bytes);
218
219 if (bytes == 0) [[unlikely]] {
220 return nullptr;
221 }
222
223 HELIOS_VERIFY(IsPowerOfTwo(alignment),
224 "alignment must be a power of two, got {}!", alignment);
225
226 const size_t effective_alignment = std::max(alignment, kMinAlignment);
227 Reservation reservation{};
228 for (;;) {
229 Block* const head = head_.load(std::memory_order_acquire);
230 if (head != nullptr) {
231 reservation = TryReserve(*head, bytes, effective_alignment);
232 if (reservation.ptr != nullptr) {
233 break;
234 }
235 }
236
237 constexpr size_t kHeader = sizeof(AllocationHeader);
238 const size_t min_capacity =
239 SaturatingAdd(SaturatingAdd(bytes, effective_alignment), kHeader);
240 HELIOS_VERIFY(EnsureCapacity(min_capacity),
241 "Unable to grow stack allocator block chain!");
242 }
243
244 allocation_count_.fetch_add(1, std::memory_order_relaxed);
245 total_allocations_.fetch_add(1, std::memory_order_relaxed);
246 total_allocated_.fetch_add(reservation.consumed, std::memory_order_relaxed);
247 alignment_waste_.fetch_add(reservation.consumed - reservation.allocation_size,
248 std::memory_order_relaxed);
249 const size_t total = total_allocated_.load(std::memory_order_relaxed);
250 details::AccumulatePeak(peak_usage_, total);
251
252 return reservation.ptr;
253}
254
255void StackAllocator::do_deallocate(void* ptr, size_t bytes,
256 size_t /*alignment*/) {
257 HELIOS_MEMORY_PROFILE_SCOPE_N("helios::mem::StackAllocator::do_deallocate");
258 HELIOS_MEMORY_PROFILE_ZONE_VALUE(bytes);
259
260 if (ptr == nullptr) [[unlikely]] {
261 return;
262 }
263
264 Block* const head = head_.load(std::memory_order_acquire);
265 if (head == nullptr) {
266 return;
267 }
268
269 const auto addr = reinterpret_cast<uintptr_t>(ptr);
270 const auto begin = reinterpret_cast<uintptr_t>(head->buffer);
271 const auto end = begin + head->capacity;
272 if (addr < begin || addr >= end) {
273 return;
274 }
275
276 auto* const header = std::launder(reinterpret_cast<AllocationHeader*>(
277 static_cast<std::byte*>(ptr) - sizeof(AllocationHeader)));
278 size_t expected_end =
279 SaturatingAdd(header->previous_offset, header->total_size);
280 if (head->offset.compare_exchange_strong(
281 expected_end, header->previous_offset, std::memory_order_acq_rel,
282 std::memory_order_acquire)) {
283 allocation_count_.fetch_sub(1, std::memory_order_relaxed);
284 total_deallocations_.fetch_add(1, std::memory_order_relaxed);
285 total_allocated_.fetch_sub(header->total_size, std::memory_order_relaxed);
286
287 if (bytes > 0) {
288 const size_t waste =
289 header->total_size > bytes ? header->total_size - bytes : 0;
290 alignment_waste_.fetch_sub(
291 std::min(waste, alignment_waste_.load(std::memory_order_relaxed)),
292 std::memory_order_relaxed);
293 }
294 }
295}
296
297} // 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
PMR stack allocator with LIFO-aware deallocation.
void RewindToMarker(Marker marker) noexcept
Rewinds allocator to marker.
void Reset() noexcept
Resets allocator to a fresh single initial block.
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.
void AlignedFree(void *ptr, bool enable_profile=true) noexcept
Frees memory allocated with AlignedAlloc.
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
Marker for rewind operations.