Helios Engine
A modular ECS based data-oriented C++23 game engine framework
Loading...
Searching...
No Matches
arena_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 <atomic>
12#include <cstddef>
13#include <memory>
14#include <utility>
15
16namespace helios::mem {
17
19 : initial_capacity_(options.initial_capacity), growth_(options.growth) {
20 HELIOS_ASSERT(initial_capacity_ > 0,
21 "initial_capacity must be greater than zero!");
22 HELIOS_ASSERT(growth_.max_capacity >= initial_capacity_,
23 "max_capacity '{}' must be >= initial_capacity '{}'!",
24 growth_.max_capacity, initial_capacity_);
25
26 Block* const initial_block = CreateBlock(initial_capacity_);
27 HELIOS_VERIFY(initial_block != nullptr, "Failed to allocate initial block!");
28
29 head_.store(initial_block, std::memory_order_release);
30 total_capacity_.store(initial_capacity_, std::memory_order_relaxed);
31 block_count_.store(1, std::memory_order_relaxed);
32}
33
34void ArenaAllocator::Reset() noexcept {
35 HELIOS_MEMORY_PROFILE_SCOPE_N("helios::mem::ArenaAllocator::Reset");
36
37 Block* current = head_.load(std::memory_order_acquire);
38 while (current != nullptr) {
39 current->offset.store(0, std::memory_order_release);
40 current = current->next.load(std::memory_order_acquire);
41 }
42
43 total_allocated_.store(0, std::memory_order_release);
44 peak_usage_.store(0, std::memory_order_release);
45 allocation_count_.store(0, std::memory_order_release);
46 total_allocations_.store(0, std::memory_order_release);
47 total_deallocations_.store(0, std::memory_order_release);
48 alignment_waste_.store(0, std::memory_order_release);
49}
50
51void ArenaAllocator::MoveFrom(ArenaAllocator& other) noexcept {
52 head_.store(other.head_.exchange(nullptr, std::memory_order_acq_rel),
53 std::memory_order_release);
54 grow_state_.store(
55 other.grow_state_.exchange(GrowState::kIdle, std::memory_order_acq_rel),
56 std::memory_order_release);
57 initial_capacity_ = std::exchange(other.initial_capacity_, 0);
58 growth_ = std::exchange(other.growth_, {});
59 total_capacity_.store(
60 other.total_capacity_.exchange(0, std::memory_order_acq_rel),
61 std::memory_order_release);
62 total_allocated_.store(
63 other.total_allocated_.exchange(0, std::memory_order_acq_rel),
64 std::memory_order_release);
65 peak_usage_.store(other.peak_usage_.exchange(0, std::memory_order_acq_rel),
66 std::memory_order_release);
67 allocation_count_.store(
68 other.allocation_count_.exchange(0, std::memory_order_acq_rel),
69 std::memory_order_release);
70 total_allocations_.store(
71 other.total_allocations_.exchange(0, std::memory_order_acq_rel),
72 std::memory_order_release);
73 total_deallocations_.store(
74 other.total_deallocations_.exchange(0, std::memory_order_acq_rel),
75 std::memory_order_release);
76 alignment_waste_.store(
77 other.alignment_waste_.exchange(0, std::memory_order_acq_rel),
78 std::memory_order_release);
79 block_count_.store(other.block_count_.exchange(0, std::memory_order_acq_rel),
80 std::memory_order_release);
81}
82
83auto ArenaAllocator::CreateBlock(size_t capacity) noexcept -> Block* {
84 constexpr size_t kHeaderSize = AlignUp(sizeof(Block), kDefaultAlignment);
85 const size_t total_size = SaturatingAdd(kHeaderSize, capacity);
86
87 void* const raw = AlignedAlloc(kDefaultAlignment, total_size, false);
88 if (raw == nullptr) [[unlikely]] {
89 return nullptr;
90 }
91
92 HELIOS_MEMORY_PROFILE_ALLOC(raw, total_size, "ArenaAllocator");
93
94 auto* const block = std::construct_at(static_cast<Block*>(raw));
95 block->buffer = static_cast<std::byte*>(raw) + kHeaderSize;
96 block->capacity = capacity;
97 block->offset.store(0, std::memory_order_relaxed);
98 block->next.store(nullptr, std::memory_order_relaxed);
99 return block;
100}
101
102void ArenaAllocator::PublishBlock(Block* block) noexcept {
103 Block* expected_head = head_.load(std::memory_order_acquire);
104 do {
105 block->next.store(expected_head, std::memory_order_relaxed);
106 } while (!head_.compare_exchange_weak(expected_head, block,
107 std::memory_order_release,
108 std::memory_order_acquire));
109
110 total_capacity_.fetch_add(block->capacity, std::memory_order_relaxed);
111 block_count_.fetch_add(1, std::memory_order_relaxed);
112}
113
114void ArenaAllocator::FreeChain(Block* head) noexcept {
115 if (head == nullptr) [[unlikely]] {
116 return;
117 }
118
119 Block* current = head;
120 while (current != nullptr) {
121 Block* const next = current->next.load(std::memory_order_relaxed);
122 HELIOS_MEMORY_PROFILE_FREE(current, "ArenaAllocator");
123 std::destroy_at(current);
124 AlignedFree(current, false);
125 current = next;
126 }
127}
128
129auto ArenaAllocator::TryReserve(Block& block, size_t size,
130 size_t alignment) noexcept -> Reservation {
131 size_t current_offset = block.offset.load(std::memory_order_acquire);
132 for (;;) {
133 auto* const begin = static_cast<std::byte*>(block.buffer) + current_offset;
134 const size_t padding = CalculatePadding(begin, alignment);
135 const size_t aligned_offset = SaturatingAdd(current_offset, padding);
136 const size_t end_offset = SaturatingAdd(aligned_offset, size);
137
138 if (end_offset > block.capacity) {
139 return {};
140 }
141
142 if (block.offset.compare_exchange_weak(current_offset, end_offset,
143 std::memory_order_release,
144 std::memory_order_acquire)) {
145 return {
146 .ptr = static_cast<std::byte*>(block.buffer) + aligned_offset,
147 .size = size,
148 .padding = padding,
149 };
150 }
151 }
152}
153
154bool ArenaAllocator::EnsureCapacity(size_t min_capacity) noexcept {
155 Block* observed_head = head_.load(std::memory_order_acquire);
156 const size_t current_capacity =
157 observed_head != nullptr ? observed_head->capacity : initial_capacity_;
158 const size_t desired_capacity =
159 growth_.NextCapacity(current_capacity, min_capacity);
160
161 if (desired_capacity < min_capacity || desired_capacity == 0) {
162 return false;
163 }
164
165 auto expected = GrowState::kIdle;
166 if (!grow_state_.compare_exchange_strong(expected, GrowState::kGrowing,
167 std::memory_order_acq_rel,
168 std::memory_order_acquire)) {
169 grow_state_.wait(GrowState::kGrowing, std::memory_order_acquire);
170 return true;
171 }
172
173 Block* new_block = CreateBlock(desired_capacity);
174 if (new_block == nullptr) {
175 grow_state_.store(GrowState::kIdle, std::memory_order_release);
176 grow_state_.notify_all();
177 return false;
178 }
179
180 PublishBlock(new_block);
181 grow_state_.store(GrowState::kIdle, std::memory_order_release);
182 grow_state_.notify_all();
183 return true;
184}
185
186void* ArenaAllocator::do_allocate(size_t bytes, size_t alignment) {
187 HELIOS_MEMORY_PROFILE_SCOPE_N("helios::mem::ArenaAllocator::do_allocate");
188 HELIOS_MEMORY_PROFILE_ZONE_VALUE(bytes);
189
190 if (bytes == 0) [[unlikely]] {
191 return nullptr;
192 }
193
194 HELIOS_VERIFY(IsPowerOfTwo(alignment),
195 "alignment must be a power of two, got {}!", alignment);
196
197 const size_t effective_alignment = std::max(alignment, kMinAlignment);
198 Reservation reservation{};
199 for (;;) {
200 Block* const head = head_.load(std::memory_order_acquire);
201 if (head != nullptr) {
202 reservation = TryReserve(*head, bytes, effective_alignment);
203 if (reservation.ptr != nullptr) {
204 break;
205 }
206 }
207
208 const size_t min_capacity = SaturatingAdd(bytes, effective_alignment);
209 HELIOS_VERIFY(EnsureCapacity(min_capacity),
210 "Unable to grow arena block chain!");
211 }
212
213 allocation_count_.fetch_add(1, std::memory_order_relaxed);
214 total_allocations_.fetch_add(1, std::memory_order_relaxed);
215 alignment_waste_.fetch_add(reservation.padding, std::memory_order_relaxed);
216 const size_t total =
217 total_allocated_.fetch_add(bytes, std::memory_order_relaxed) + bytes;
218 details::AccumulatePeak(peak_usage_, total);
219
220 return reservation.ptr;
221}
222
223} // 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 arena allocator with lock-free hot allocation path.
ArenaAllocator(ArenaOptions options={}) noexcept
Constructs arena allocator from options.
void Reset() noexcept
Soft-resets all block bump pointers to zero.
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
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.
size_t CalculatePadding(const void *ptr, size_t alignment) noexcept
Computes padding required for pointer alignment.
Definition common.hpp:274
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
Configuration for ArenaAllocator.