Helios Engine
A modular ECS based data-oriented C++23 game engine framework
Loading...
Searching...
No Matches
stack_allocator.hpp
Go to the documentation of this file.
1#pragma once
2
4
5#include <helios/assert.hpp>
6#include <helios/memory/details/profile.hpp>
7
8#include <atomic>
9#include <cstddef>
10#include <cstdint>
11#include <memory_resource>
12
13namespace helios::mem {
14
15/// @brief Configuration for StackAllocator.
17 /// @brief Capacity of the initial backing block in bytes.
18 size_t initial_capacity = 0;
19 /// @brief Growth policy applied when additional blocks are required.
21};
22
23/**
24 * @brief PMR stack allocator with LIFO-aware deallocation.
25 * @details Uses block-chained bump allocation with per-allocation headers.
26 *
27 * Allocation path is lock-free with atomic CAS on head block offset. Deallocate
28 * tries LIFO rewind when possible. Non-LIFO deallocation is treated as no-op,
29 * which keeps PMR compatibility for containers that may free out of order.
30 *
31 * @note `allocate` and LIFO `deallocate` on the current head are lock-free.
32 * `Reset`, `GetMarker`, and `RewindToMarker` require external synchronization.
33 * `Marker.block` is internal and invalid after growth.
34 */
35class StackAllocator final : public std::pmr::memory_resource {
36public:
37 /// @brief Marker for rewind operations.
38 struct Marker {
39 /// @brief Block containing the marker offset; invalid after growth.
40 void* block = nullptr;
41 /// @brief Bump offset within `block` at marker capture time.
42 size_t offset = 0;
43 };
44
45 /**
46 * @brief Constructs stack allocator from options.
47 * @warning Triggers assertion if `options.initial_capacity == 0` or
48 * `options.growth.max_capacity < options.initial_capacity`.
49 * @param options Stack allocator options
50 */
51 explicit StackAllocator(StackAllocatorOptions options) noexcept;
52
53 /**
54 * @brief Constructs stack allocator with geometric growth.
55 * @warning Triggers assertion if `initial_capacity == 0`.
56 * @param initial_capacity Initial block capacity
57 */
58 explicit StackAllocator(size_t initial_capacity) noexcept
60 StackAllocatorOptions{.initial_capacity = initial_capacity,
61 .growth = GrowthPolicy::Geometric()}) {}
62
64
65 /**
66 * @brief Move-constructs stack allocator from another instance.
67 * @param other Source allocator; left in empty moved-from state
68 */
69 StackAllocator(StackAllocator&& other) noexcept { MoveFrom(other); }
70 ~StackAllocator() noexcept override {
71 FreeChain(head_.load(std::memory_order_acquire));
72 }
73
75
76 /**
77 * @brief Move-assigns stack state from another instance.
78 * @param other Source allocator; left in empty moved-from state
79 * @return Reference to this allocator
80 */
81 StackAllocator& operator=(StackAllocator&& other) noexcept;
82
83 /**
84 * @brief Rewinds allocator to marker.
85 * @warning Must not be called concurrently with allocation/deallocation.
86 * Triggers assertion if marker does not belong to this allocator.
87 * @param marker Marker previously returned by `GetMarker`
88 */
89 void RewindToMarker(Marker marker) noexcept;
90
91 /**
92 * @brief Resets allocator to a fresh single initial block.
93 * @warning Must not be called concurrently with allocation/deallocation.
94 */
95 void Reset() noexcept;
96
97 /**
98 * @brief Returns true when no active allocations remain.
99 * @return True if empty, false otherwise.
100 */
101 [[nodiscard]] bool Empty() const noexcept {
102 return allocation_count_.load(std::memory_order_relaxed) == 0;
103 }
104
105 /**
106 * @brief Returns current marker.
107 * @return Marker that can later be rewound to
108 */
109 [[nodiscard]] Marker GetMarker() const noexcept;
110
111 /**
112 * @brief Returns runtime statistics.
113 * @return Allocator stats snapshot
114 */
115 [[nodiscard]] AllocatorStats Stats() const noexcept;
116
117 /**
118 * @brief Returns initial block capacity.
119 * @return Capacity in bytes
120 */
121 [[nodiscard]] size_t InitialCapacity() const noexcept {
122 return initial_capacity_;
123 }
124
125 /**
126 * @brief Returns total capacity across block chain.
127 * @return Capacity in bytes
128 */
129 [[nodiscard]] size_t TotalCapacity() const noexcept {
130 return total_capacity_.load(std::memory_order_relaxed);
131 }
132
133 /**
134 * @brief Returns current number of blocks.
135 * @return Block count
136 */
137 [[nodiscard]] size_t BlockCount() const noexcept {
138 return block_count_.load(std::memory_order_relaxed);
139 }
140
141 /**
142 * @brief Returns growth policy.
143 * @return Growth policy
144 */
145 [[nodiscard]] GrowthPolicy Growth() const noexcept { return growth_; }
146
147private:
148 struct AllocationHeader {
149 size_t previous_offset = 0;
150 size_t total_size = 0;
151 };
152
153 struct Block {
154 void* buffer = nullptr;
155 size_t capacity = 0;
156 std::atomic<size_t> offset{0};
157 std::atomic<Block*> next{nullptr};
158 };
159
160 struct Reservation {
161 void* ptr = nullptr;
162 size_t allocation_size = 0;
163 size_t consumed = 0;
164 size_t previous_offset = 0;
165 };
166
167 enum class GrowState : uint8_t {
168 kIdle,
169 kGrowing,
170 };
171
172 void MoveFrom(StackAllocator& other) noexcept;
173
174 [[nodiscard]] static Block* CreateBlock(size_t capacity) noexcept;
175 static void FreeChain(Block* head) noexcept;
176 [[nodiscard]] static Reservation TryReserve(Block& block, size_t size,
177 size_t alignment) noexcept;
178 [[nodiscard]] bool EnsureCapacity(size_t min_capacity) noexcept;
179 void PublishBlock(Block* block) noexcept;
180
181 [[nodiscard]] void* do_allocate(size_t bytes, size_t alignment) override;
182 void do_deallocate(void* ptr, size_t bytes, size_t alignment) override;
183 [[nodiscard]] bool do_is_equal(
184 const std::pmr::memory_resource& other) const noexcept override {
185 return this == &other;
186 }
187
188 std::atomic<Block*> head_{nullptr};
189 std::atomic<GrowState> grow_state_{GrowState::kIdle};
190 size_t initial_capacity_ = 0;
191 GrowthPolicy growth_{};
192
193 std::atomic<size_t> total_capacity_{0};
194 std::atomic<size_t> total_allocated_{0};
195 std::atomic<size_t> peak_usage_{0};
196 std::atomic<size_t> allocation_count_{0};
197 std::atomic<size_t> total_allocations_{0};
198 std::atomic<size_t> total_deallocations_{0};
199 std::atomic<size_t> alignment_waste_{0};
200 std::atomic<size_t> block_count_{0};
201};
202
204 : initial_capacity_(options.initial_capacity), growth_(options.growth) {
205 HELIOS_ASSERT(initial_capacity_ > 0,
206 "initial_capacity must be greater than zero!");
207 HELIOS_ASSERT(growth_.max_capacity >= initial_capacity_,
208 "max_capacity '{}' must be >= initial_capacity '{}'!",
209 growth_.max_capacity, initial_capacity_);
210
211 Block* const initial_block = CreateBlock(initial_capacity_);
212 HELIOS_VERIFY(initial_block != nullptr, "Failed to allocate initial block!");
213 head_.store(initial_block, std::memory_order_release);
214 total_capacity_.store(initial_capacity_, std::memory_order_relaxed);
215 block_count_.store(1, std::memory_order_relaxed);
216}
217
219 StackAllocator&& other) noexcept {
220 if (this == &other) [[unlikely]] {
221 return *this;
222 }
223
224 FreeChain(head_.load(std::memory_order_acquire));
225 MoveFrom(other);
226 return *this;
227}
228
229inline auto StackAllocator::GetMarker() const noexcept -> Marker {
230 Block* const head = head_.load(std::memory_order_acquire);
231 if (head == nullptr) {
232 return {};
233 }
234
235 return {
236 .block = head,
237 .offset = head->offset.load(std::memory_order_acquire),
238 };
239}
240
241inline AllocatorStats StackAllocator::Stats() const noexcept {
242 return {
243 .total_allocated = total_allocated_.load(std::memory_order_relaxed),
244 .peak_usage = peak_usage_.load(std::memory_order_relaxed),
245 .allocation_count = allocation_count_.load(std::memory_order_relaxed),
246 .total_allocations = total_allocations_.load(std::memory_order_relaxed),
247 .total_deallocations =
248 total_deallocations_.load(std::memory_order_relaxed),
249 .alignment_waste = alignment_waste_.load(std::memory_order_relaxed),
250 };
251}
252
253} // 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
Marker GetMarker() const noexcept
Returns current marker.
void RewindToMarker(Marker marker) noexcept
Rewinds allocator to marker.
bool Empty() const noexcept
Returns true when no active allocations remain.
GrowthPolicy Growth() const noexcept
Returns growth policy.
StackAllocator(StackAllocator &&other) noexcept
Move-constructs stack allocator from another instance.
StackAllocator(size_t initial_capacity) noexcept
Constructs stack allocator with geometric growth.
AllocatorStats Stats() const noexcept
Returns runtime statistics.
size_t InitialCapacity() const noexcept
Returns initial block capacity.
~StackAllocator() noexcept override
StackAllocator & operator=(const StackAllocator &)=delete
StackAllocator(const StackAllocator &)=delete
size_t TotalCapacity() const noexcept
Returns total capacity across block chain.
void Reset() noexcept
Resets allocator to a fresh single initial block.
size_t BlockCount() const noexcept
Returns current number of blocks.
StackAllocator(StackAllocatorOptions options) noexcept
Constructs stack allocator from options.
constexpr AllocatorStats Stats(const Alloc &allocator) noexcept
Adapts stats-aware PMR resources.
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 StackAllocator.
size_t initial_capacity
Capacity of the initial backing block in bytes.
GrowthPolicy growth
Growth policy applied when additional blocks are required.
Marker for rewind operations.
size_t offset
Bump offset within block at marker capture time.
void * block
Block containing the marker offset; invalid after growth.