Helios Engine
A modular ECS based data-oriented C++23 game engine framework
Loading...
Searching...
No Matches
arena_allocator.hpp
Go to the documentation of this file.
1#pragma once
2
4
5#include <atomic>
6#include <cstddef>
7#include <cstdint>
8#include <memory_resource>
9
10namespace helios::mem {
11
12/**
13 * @brief Configuration for ArenaAllocator.
14 * @details Controls initial capacity and growth behavior.
15 */
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 arena allocator with lock-free hot allocation path.
25 * @details Individual deallocation is a no-op, matching monotonic arena
26 * semantics. Memory is reclaimed by `Reset()` or destruction.
27 * @note Thread-safe for `allocate` and `deallocate`.
28 * @warning `Reset()` must not run concurrently with allocation/deallocation.
29 */
30class ArenaAllocator final : public std::pmr::memory_resource {
31public:
32 /**
33 * @brief Constructs arena allocator from options.
34 * @warning Triggers assertion if `options.initial_capacity == 0` or
35 * `options.growth.max_capacity < options.initial_capacity`.
36 * @param options Arena configuration
37 */
38 explicit ArenaAllocator(ArenaOptions options = {}) noexcept;
39
40 /**
41 * @brief Constructs arena allocator with geometric growth.
42 * @warning Triggers assertion if `initial_capacity == 0`.
43 * @param initial_capacity Capacity of initial block in bytes
44 */
45 explicit ArenaAllocator(size_t initial_capacity) noexcept
46 : ArenaAllocator(ArenaOptions{.initial_capacity = initial_capacity,
47 .growth = GrowthPolicy::Geometric()}) {}
48
50
51 /**
52 * @brief Move-constructs arena allocator from another instance.
53 * @param other Source allocator; left in empty moved-from state
54 */
55 ArenaAllocator(ArenaAllocator&& other) noexcept { MoveFrom(other); }
56 ~ArenaAllocator() noexcept override {
57 FreeChain(head_.load(std::memory_order_acquire));
58 }
59
61
62 /**
63 * @brief Move-assigns arena state from another instance.
64 * @param other Source allocator; left in empty moved-from state
65 * @return Reference to this allocator
66 */
67 ArenaAllocator& operator=(ArenaAllocator&& other) noexcept;
68
69 /**
70 * @brief Soft-resets all block bump pointers to zero.
71 * @details All existing allocations become logically invalid. Blocks are
72 * kept allocated and offsets reset to the start, allowing immediate reuse.
73 * @warning Must not be called concurrently with `allocate`.
74 */
75 void Reset() noexcept;
76
77 /**
78 * @brief Returns true when no allocations are currently active.
79 * @details Uses a monotonic allocation counter; stays false after any
80 * allocation until `Reset()` even though individual deallocations are no-ops.
81 * @return True if arena is empty, false otherwise
82 */
83 [[nodiscard]] bool Empty() const noexcept {
84 return allocation_count_.load(std::memory_order_relaxed) == 0;
85 }
86
87 /**
88 * @brief Returns allocator statistics snapshot.
89 * @return AllocatorStats for this arena
90 */
91 [[nodiscard]] AllocatorStats Stats() const noexcept;
92
93 /**
94 * @brief Returns total capacity across all active blocks.
95 * @return Capacity in bytes
96 */
97 [[nodiscard]] size_t TotalCapacity() const noexcept {
98 return total_capacity_.load(std::memory_order_relaxed);
99 }
100
101 /**
102 * @brief Returns initial block capacity.
103 * @return Initial capacity in bytes
104 */
105 [[nodiscard]] size_t InitialCapacity() const noexcept {
106 return initial_capacity_;
107 }
108
109 /**
110 * @brief Returns current number of blocks.
111 * @return Block count
112 */
113 [[nodiscard]] size_t BlockCount() const noexcept {
114 return block_count_.load(std::memory_order_relaxed);
115 }
116
117 /**
118 * @brief Returns configured growth policy.
119 * @return Growth policy
120 */
121 [[nodiscard]] GrowthPolicy Growth() const noexcept { return growth_; }
122
123private:
124 struct Block {
125 void* buffer = nullptr;
126 size_t capacity = 0;
127 std::atomic<size_t> offset{0};
128 std::atomic<Block*> next{nullptr};
129 };
130
131 struct Reservation {
132 void* ptr = nullptr;
133 size_t size = 0;
134 size_t padding = 0;
135 };
136
137 enum class GrowState : uint8_t { kIdle, kGrowing };
138
139 void MoveFrom(ArenaAllocator& other) noexcept;
140
141 [[nodiscard]] static Block* CreateBlock(size_t capacity) noexcept;
142 void PublishBlock(Block* block) noexcept;
143 static void FreeChain(Block* head) noexcept;
144
145 [[nodiscard]] static Reservation TryReserve(Block& block, size_t size,
146 size_t alignment) noexcept;
147
148 [[nodiscard]] bool EnsureCapacity(size_t min_capacity) noexcept;
149
150 [[nodiscard]] void* do_allocate(size_t bytes, size_t alignment) override;
151 void do_deallocate(void* /*pointer*/, size_t /*bytes*/,
152 size_t /*alignment*/) override {
153 total_deallocations_.fetch_add(1, std::memory_order_relaxed);
154 }
155
156 [[nodiscard]] bool do_is_equal(
157 const std::pmr::memory_resource& other) const noexcept override {
158 return this == &other;
159 }
160
161 std::atomic<Block*> head_{nullptr};
162 std::atomic<GrowState> grow_state_{GrowState::kIdle};
163 size_t initial_capacity_ = 0;
164 GrowthPolicy growth_;
165
166 std::atomic<size_t> total_capacity_{0};
167 std::atomic<size_t> total_allocated_{0};
168 std::atomic<size_t> peak_usage_{0};
169 std::atomic<size_t> allocation_count_{0};
170 std::atomic<size_t> total_allocations_{0};
171 std::atomic<size_t> total_deallocations_{0};
172 std::atomic<size_t> alignment_waste_{0};
173 std::atomic<size_t> block_count_{0};
174};
175
177 ArenaAllocator&& other) noexcept {
178 if (this == &other) [[unlikely]] {
179 return *this;
180 }
181
182 FreeChain(head_.load(std::memory_order_acquire));
183 MoveFrom(other);
184 return *this;
185}
186
187inline AllocatorStats ArenaAllocator::Stats() const noexcept {
188 return {
189 .total_allocated = total_allocated_.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
ArenaAllocator(size_t initial_capacity) noexcept
Constructs arena allocator with geometric growth.
ArenaAllocator(ArenaOptions options={}) noexcept
Constructs arena allocator from options.
~ArenaAllocator() noexcept override
AllocatorStats Stats() const noexcept
Returns allocator statistics snapshot.
void Reset() noexcept
Soft-resets all block bump pointers to zero.
size_t InitialCapacity() const noexcept
Returns initial block capacity.
GrowthPolicy Growth() const noexcept
Returns configured growth policy.
size_t BlockCount() const noexcept
Returns current number of blocks.
ArenaAllocator & operator=(const ArenaAllocator &)=delete
size_t TotalCapacity() const noexcept
Returns total capacity across all active blocks.
bool Empty() const noexcept
Returns true when no allocations are currently active.
ArenaAllocator(const ArenaAllocator &)=delete
ArenaAllocator(ArenaAllocator &&other) noexcept
Move-constructs arena allocator from another instance.
Runtime statistics snapshot for PMR allocators.
Definition common.hpp:163
Configuration for ArenaAllocator.
GrowthPolicy growth
Growth policy applied when additional blocks are required.
size_t initial_capacity
Capacity of the initial backing block in bytes.
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