Helios Engine
A modular ECS based data-oriented C++23 game engine framework
Loading...
Searching...
No Matches
fixed_stack_allocator.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <helios/assert.hpp>
6#include <helios/memory/details/profile.hpp>
7
8#include <atomic>
9#include <cstddef>
10#include <memory_resource>
11
12namespace helios::mem {
13
14/**
15 * @brief PMR stack allocator with fixed runtime capacity.
16 * @details LIFO-aware deallocation on the current top allocation uses CAS on
17 * the bump offset. Non-LIFO frees are no-ops for PMR compatibility.
18 * @note `allocate` and LIFO `deallocate` on the current head are lock-free.
19 * @warning `Reset()` and `RewindToMarker()` must not run concurrently with
20 * `allocate` or `deallocate`.
21 */
22class FixedStackAllocator final : public std::pmr::memory_resource {
23public:
24 /// @brief Marker capturing the current bump offset for rewind operations.
25 struct Marker {
26 /// @brief Bump offset at marker capture time.
27 size_t offset = 0;
28 };
29
30 /**
31 * @brief Constructs fixed stack with the given backing capacity.
32 * @warning Triggers assertion if capacity is too small. Triggers
33 * verification failure if backing allocation fails.
34 * @param capacity Backing buffer size in bytes
35 */
36 explicit FixedStackAllocator(size_t capacity) noexcept;
38
39 /**
40 * @brief Move-constructs fixed stack from another instance.
41 * @param other Source allocator; left in empty moved-from state
42 */
43 FixedStackAllocator(FixedStackAllocator&& other) noexcept { MoveFrom(other); }
44 ~FixedStackAllocator() noexcept override { Release(); }
45
47
48 /**
49 * @brief Move-assigns fixed stack state from another instance.
50 * @param other Source allocator; left in empty moved-from state
51 * @return Reference to this allocator
52 */
54
55 /**
56 * @brief Rewinds bump offset to marker.
57 * @warning Must not be called concurrently with `allocate` or `deallocate`.
58 * Triggers assertion when `marker.offset` exceeds the current offset.
59 * @param marker Marker previously returned by `GetMarker`
60 */
61 void RewindToMarker(Marker marker) noexcept;
62
63 /**
64 * @brief Soft-resets bump offset and statistics.
65 * @warning Must not be called concurrently with `allocate` or `deallocate`.
66 */
67 void Reset() noexcept;
68
69 /**
70 * @brief Returns true when no active allocations remain.
71 * @return True if empty, false otherwise
72 */
73 [[nodiscard]] bool Empty() const noexcept {
74 return offset_.load(std::memory_order_relaxed) == 0;
75 }
76
77 /**
78 * @brief Checks whether pointer belongs to this stack's backing buffer.
79 * @param ptr Pointer to test
80 * @return True when owned, false otherwise
81 */
82 [[nodiscard]] bool Owns(const void* ptr) const noexcept;
83
84 /**
85 * @brief Returns current marker.
86 * @return Marker that can later be rewound to
87 */
88 [[nodiscard]] Marker GetMarker() const noexcept {
89 return {offset_.load(std::memory_order_acquire)};
90 }
91
92 /**
93 * @brief Returns runtime statistics snapshot.
94 * @return Allocator stats for this stack
95 */
96 [[nodiscard]] AllocatorStats Stats() const noexcept;
97
98 /**
99 * @brief Returns configured backing capacity.
100 * @return Capacity in bytes
101 */
102 [[nodiscard]] size_t InitialCapacity() const noexcept { return capacity_; }
103
104 /**
105 * @brief Returns configured backing capacity.
106 * @return Capacity in bytes
107 */
108 [[nodiscard]] size_t TotalCapacity() const noexcept { return capacity_; }
109
110private:
111 struct AllocationHeader {
112 size_t previous_offset = 0;
113 size_t end_offset = 0;
114 };
115
116 void MoveFrom(FixedStackAllocator& other) noexcept;
117
118 void ClearStats() noexcept;
119 void Release() noexcept;
120
121 [[nodiscard]] void* do_allocate(size_t bytes, size_t alignment) override;
122 void do_deallocate(void* ptr, size_t bytes, size_t alignment) override;
123 [[nodiscard]] bool do_is_equal(
124 const std::pmr::memory_resource& other) const noexcept override {
125 return this == &other;
126 }
127
128 size_t capacity_ = 0;
129 std::byte* buffer_ = nullptr;
130 std::atomic<size_t> offset_{0};
131 std::atomic<size_t> peak_usage_{0};
132 std::atomic<size_t> allocation_count_{0};
133 std::atomic<size_t> total_allocations_{0};
134 std::atomic<size_t> total_deallocations_{0};
135 std::atomic<size_t> alignment_waste_{0};
136};
137
138inline FixedStackAllocator::FixedStackAllocator(size_t capacity) noexcept
139 : capacity_(capacity) {
140 HELIOS_ASSERT(capacity_ > sizeof(size_t) * 2,
141 "capacity '{}' is too small for fixed stack!", capacity_);
142 buffer_ = static_cast<std::byte*>(
143 AlignedAlloc(kDefaultAlignment, capacity_, false));
144 HELIOS_VERIFY(buffer_ != nullptr, "Failed to allocate fixed stack!");
145 HELIOS_MEMORY_PROFILE_ALLOC(buffer_, capacity_, "FixedStackAllocator");
146}
147
149 FixedStackAllocator&& other) noexcept {
150 if (this == &other) [[unlikely]] {
151 return *this;
152 }
153
154 Release();
155 MoveFrom(other);
156 return *this;
157}
158
159inline void FixedStackAllocator::RewindToMarker(Marker marker) noexcept {
160 HELIOS_MEMORY_PROFILE_SCOPE_N(
161 "helios::mem::FixedStackAllocator::RewindToMarker");
162
163 HELIOS_ASSERT(marker.offset <= offset_.load(std::memory_order_acquire),
164 "marker does not belong to fixed stack!");
165
166 offset_.store(marker.offset, std::memory_order_release);
167 allocation_count_.store(0, std::memory_order_relaxed);
168}
169
170inline void FixedStackAllocator::Reset() noexcept {
171 HELIOS_MEMORY_PROFILE_SCOPE_N("helios::mem::FixedStackAllocator::Reset");
172 ClearStats();
173}
174
175inline bool FixedStackAllocator::Owns(const void* ptr) const noexcept {
176 HELIOS_MEMORY_PROFILE_SCOPE_N("helios::mem::FixedStackAllocator::Owns");
177
178 if (buffer_ == nullptr || ptr == nullptr) [[unlikely]] {
179 return false;
180 }
181
182 const auto address = reinterpret_cast<uintptr_t>(ptr);
183 const auto begin = reinterpret_cast<uintptr_t>(buffer_);
184 return address >= begin && address < begin + capacity_;
185}
186
188 return {
189 .total_allocated = offset_.load(std::memory_order_relaxed),
190 .peak_usage = peak_usage_.load(std::memory_order_relaxed),
191 .allocation_count = allocation_count_.load(std::memory_order_relaxed),
192 .total_allocations = total_allocations_.load(std::memory_order_relaxed),
193 .total_deallocations =
194 total_deallocations_.load(std::memory_order_relaxed),
195 .alignment_waste = alignment_waste_.load(std::memory_order_relaxed),
196 };
197}
198
199} // 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
size_t TotalCapacity() const noexcept
Returns configured backing capacity.
bool Owns(const void *ptr) const noexcept
Checks whether pointer belongs to this stack's backing buffer.
FixedStackAllocator(size_t capacity) noexcept
Constructs fixed stack with the given backing capacity.
void RewindToMarker(Marker marker) noexcept
Rewinds bump offset to marker.
bool Empty() const noexcept
Returns true when no active allocations remain.
void Reset() noexcept
Soft-resets bump offset and statistics.
FixedStackAllocator(FixedStackAllocator &&other) noexcept
Move-constructs fixed stack from another instance.
FixedStackAllocator & operator=(const FixedStackAllocator &)=delete
FixedStackAllocator(const FixedStackAllocator &)=delete
size_t InitialCapacity() const noexcept
Returns configured backing capacity.
AllocatorStats Stats() const noexcept
Returns runtime statistics snapshot.
Marker GetMarker() const noexcept
Returns current marker.
void * AlignedAlloc(size_t alignment, size_t size, bool enable_profile=true) noexcept
Allocates memory with the specified alignment.
constexpr size_t kDefaultAlignment
Default alignment used by memory resources.
Definition common.hpp:14
STL namespace.
Runtime statistics snapshot for PMR allocators.
Definition common.hpp:163
Marker capturing the current bump offset for rewind operations.
size_t offset
Bump offset at marker capture time.