Helios Engine
A modular ECS based data-oriented C++23 game engine framework
Loading...
Searching...
No Matches
free_list_allocator.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <helios/assert.hpp>
5#include <helios/memory/details/profile.hpp>
6
7#include <atomic>
8#include <cstddef>
9#include <cstdint>
10#include <memory_resource>
11#include <mutex>
12
13namespace helios::mem {
14
15/// @brief Configuration for `FreeListAllocator`.
17 /// @brief Capacity of the initial backing region in bytes.
18 size_t initial_capacity = 0;
19 /// @brief Growth policy applied when additional regions are required.
21};
22
23/**
24 * @brief General-purpose PMR free-list allocator.
25 * @details Best-fit free-list with coalescing under a mutex. Growable backing
26 * regions retire through epoch-based reclamation so `Owns()` can scan
27 * regions without holding the free-list mutex.
28 */
29class FreeListAllocator final : public std::pmr::memory_resource {
30public:
31 /**
32 * @brief Constructs free-list allocator from options.
33 * @warning Triggers assertion if `options.initial_capacity` is too small or
34 * `options.growth.max_capacity < options.initial_capacity`.
35 * @param options Allocator options
36 */
37 explicit FreeListAllocator(FreeListAllocatorOptions options) noexcept;
38
39 /**
40 * @brief Constructs free-list allocator with geometric growth.
41 * @warning Triggers assertion when initial capacity is too small.
42 * @param initial_capacity Initial backing capacity
43 */
44 explicit FreeListAllocator(size_t initial_capacity) noexcept
46 .initial_capacity = initial_capacity,
47 .growth = GrowthPolicy::Geometric(),
48 }) {}
49
51
52 /**
53 * @brief Move-constructs free-list allocator from another instance.
54 * @param other Source allocator; left in empty moved-from state
55 */
56 FreeListAllocator(FreeListAllocator&& other) noexcept;
57 ~FreeListAllocator() noexcept override { ReleaseRegions(); }
58
60
61 /**
62 * @brief Move-assigns free-list state from another instance.
63 * @param other Source allocator; left in empty moved-from state
64 * @return Reference to this allocator
65 */
67
68 /**
69 * @brief Resets allocator to initial state.
70 * @warning Must not be called concurrently with allocate/deallocate.
71 */
72 void Reset() noexcept;
73
74 /**
75 * @brief Returns true when no allocations are active.
76 * @return True if empty, false otherwise
77 */
78 [[nodiscard]] bool Empty() const noexcept {
79 return allocation_count_.load(std::memory_order_relaxed) == 0;
80 }
81
82 /**
83 * @brief Checks whether pointer belongs to allocator storage.
84 * @param ptr Pointer to test
85 * @return True when owned, false otherwise
86 */
87 [[nodiscard]] bool Owns(const void* ptr) const noexcept;
88
89 /**
90 * @brief Returns runtime stats.
91 * @return Allocator stats snapshot
92 */
93 [[nodiscard]] AllocatorStats Stats() const noexcept;
94
95 /**
96 * @brief Returns total capacity across regions.
97 * @return Capacity in bytes
98 */
99 [[nodiscard]] size_t Capacity() const noexcept {
100 return capacity_.load(std::memory_order_relaxed);
101 }
102
103 /**
104 * @brief Returns used memory bytes.
105 * @return Used bytes
106 */
107 [[nodiscard]] size_t UsedMemory() const noexcept {
108 return used_memory_.load(std::memory_order_relaxed);
109 }
110
111 /**
112 * @brief Returns free memory bytes.
113 * @return Free bytes
114 */
115 [[nodiscard]] size_t FreeMemory() const noexcept;
116
117 /**
118 * @brief Returns free-list block count.
119 * @return Number of free blocks
120 */
121 [[nodiscard]] size_t FreeBlockCount() const noexcept {
122 return free_block_count_.load(std::memory_order_relaxed);
123 }
124
125 /**
126 * @brief Returns current active allocation count.
127 * @return Number of active allocations
128 */
129 [[nodiscard]] size_t AllocationCount() const noexcept {
130 return allocation_count_.load(std::memory_order_relaxed);
131 }
132
133 /**
134 * @brief Returns growth policy.
135 * @return Growth policy
136 */
137 [[nodiscard]] GrowthPolicy Growth() const noexcept { return growth_; }
138
139private:
140 struct FreeBlockHeader {
141 size_t size = 0;
142 FreeBlockHeader* next = nullptr;
143 };
144
145 struct RegionHeader {
146 void* buffer = nullptr;
147 size_t capacity = 0;
148 std::atomic<RegionHeader*> next{nullptr};
149 };
150
151 enum class GrowState : uint8_t { kIdle, kGrowing };
152
153 static RegionHeader* CreateRegion(size_t capacity) noexcept;
154 void FreeRegions(RegionHeader* region) noexcept;
155 void ReleaseRegions() noexcept;
156
157 [[nodiscard]] void* AllocateLocked(size_t bytes, size_t alignment) noexcept;
158 void DeallocateLocked(void* ptr) noexcept;
159 [[nodiscard]] bool EnsureCapacity(size_t min_capacity) noexcept;
160 void InsertAndCoalesceLocked(FreeBlockHeader* block) noexcept;
161 void InitializeRegionLocked(RegionHeader& region) noexcept;
162 void MoveFrom(FreeListAllocator& other) noexcept;
163
164 [[nodiscard]] void* do_allocate(size_t bytes, size_t alignment) override;
165 void do_deallocate(void* ptr, size_t bytes, size_t alignment) override;
166 [[nodiscard]] bool do_is_equal(
167 const std::pmr::memory_resource& other) const noexcept override {
168 return this == &other;
169 }
170
171 FreeBlockHeader* free_list_ = nullptr;
172 std::atomic<RegionHeader*> regions_{nullptr};
173 std::atomic<GrowState> grow_state_{GrowState::kIdle};
174 size_t initial_capacity_ = 0;
175 GrowthPolicy growth_;
176
177 std::atomic<size_t> capacity_{0};
178 std::atomic<size_t> used_memory_{0};
179 std::atomic<size_t> peak_usage_{0};
180 std::atomic<size_t> free_block_count_{0};
181 std::atomic<size_t> allocation_count_{0};
182 std::atomic<size_t> total_allocations_{0};
183 std::atomic<size_t> total_deallocations_{0};
184 std::atomic<size_t> alignment_waste_{0};
185
186 mutable HELIOS_MEMORY_PROFILE_LOCKABLE(std::mutex, mutex_);
187};
188
190 FreeListAllocatorOptions options) noexcept
191 : initial_capacity_(options.initial_capacity), growth_(options.growth) {
192 HELIOS_ASSERT(initial_capacity_ > sizeof(FreeBlockHeader),
193 "initial_capacity '{}' is too small!", initial_capacity_);
194 HELIOS_ASSERT(growth_.max_capacity >= initial_capacity_,
195 "max_capacity '{}' must be >= initial_capacity '{}'!",
196 growth_.max_capacity, initial_capacity_);
197
198 HELIOS_MEMORY_PROFILE_LOCK_NAME(mutex_,
199 std::string_view{"FreeListAllocator"});
200
201 RegionHeader* const initial_region = CreateRegion(initial_capacity_);
202 HELIOS_VERIFY(initial_region != nullptr,
203 "Failed to allocate free-list region!");
204
205 regions_.store(initial_region, std::memory_order_release);
206 capacity_.store(initial_capacity_, std::memory_order_relaxed);
207
208 {
209 const std::scoped_lock lock(mutex_);
210 InitializeRegionLocked(*initial_region);
211 }
212}
213
215 FreeListAllocator&& other) noexcept {
216 const std::scoped_lock lock(other.mutex_);
217 MoveFrom(other);
218}
219
221 FreeListAllocator&& other) noexcept {
222 if (this == &other) [[unlikely]] {
223 return *this;
224 }
225
226 const std::scoped_lock lock(mutex_, other.mutex_);
227 ReleaseRegions();
228 MoveFrom(other);
229 return *this;
230}
231
233 return {
234 .total_allocated = used_memory_.load(std::memory_order_relaxed),
235 .peak_usage = peak_usage_.load(std::memory_order_relaxed),
236 .allocation_count = allocation_count_.load(std::memory_order_relaxed),
237 .total_allocations = total_allocations_.load(std::memory_order_relaxed),
238 .total_deallocations =
239 total_deallocations_.load(std::memory_order_relaxed),
240 .alignment_waste = alignment_waste_.load(std::memory_order_relaxed),
241 };
242}
243
244} // 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 FreeBlockCount() const noexcept
Returns free-list block count.
size_t UsedMemory() const noexcept
Returns used memory bytes.
bool Empty() const noexcept
Returns true when no allocations are active.
size_t Capacity() const noexcept
Returns total capacity across regions.
FreeListAllocator(const FreeListAllocator &)=delete
FreeListAllocator & operator=(const FreeListAllocator &)=delete
GrowthPolicy Growth() const noexcept
Returns growth policy.
FreeListAllocator(size_t initial_capacity) noexcept
Constructs free-list allocator with geometric growth.
AllocatorStats Stats() const noexcept
Returns runtime stats.
size_t FreeMemory() const noexcept
Returns free memory bytes.
void Reset() noexcept
Resets allocator to initial state.
FreeListAllocator(FreeListAllocatorOptions options) noexcept
Constructs free-list allocator from options.
bool Owns(const void *ptr) const noexcept
Checks whether pointer belongs to allocator storage.
size_t AllocationCount() const noexcept
Returns current active allocation count.
Runtime statistics snapshot for PMR allocators.
Definition common.hpp:163
Configuration for FreeListAllocator.
size_t initial_capacity
Capacity of the initial backing region in bytes.
GrowthPolicy growth
Growth policy applied when additional regions are required.
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