Helios Engine
A modular ECS based data-oriented C++23 game engine framework
Loading...
Searching...
No Matches
fixed_free_list_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 <cstdint>
11#include <memory_resource>
12#include <mutex>
13#include <shared_mutex>
14#include <string_view>
15
16namespace helios::mem {
17
18/**
19 * @brief Fixed-capacity best-fit allocator with coalescing.
20 * @details Mutex-protected variable-size allocations inside one region.
21 * @warning `Reset()` must not run concurrently with `allocate` or `deallocate`.
22 */
23class FixedFreeListAllocator final : public std::pmr::memory_resource {
24public:
25 /**
26 * @brief Constructs fixed free-list with the given backing capacity.
27 * @warning Triggers assertion if capacity is too small. Triggers
28 * verification failure if backing allocation fails.
29 * @param capacity Backing buffer size in bytes
30 */
31 explicit FixedFreeListAllocator(size_t capacity) noexcept;
33
34 /**
35 * @brief Move-constructs fixed free-list from another instance.
36 * @param other Source allocator; left in empty moved-from state
37 */
39 ~FixedFreeListAllocator() noexcept override { Release(); }
40
42
43 /**
44 * @brief Move-assigns fixed free-list state from another instance.
45 * @param other Source allocator; left in empty moved-from state
46 * @return Reference to this allocator
47 */
49
50 /**
51 * @brief Resets allocator to a single free region.
52 * @warning Must not be called concurrently with `allocate` or `deallocate`.
53 */
54 void Reset() noexcept;
55
56 /**
57 * @brief Returns true when no allocations are active.
58 * @return True if empty, false otherwise
59 */
60 [[nodiscard]] bool Empty() const noexcept {
61 return allocation_count_.load(std::memory_order_relaxed) == 0;
62 }
63
64 /**
65 * @brief Checks whether pointer belongs to this allocator's backing buffer.
66 * @param ptr Pointer to test
67 * @return True when owned, false otherwise
68 */
69 [[nodiscard]] bool Owns(const void* ptr) const noexcept;
70
71 /**
72 * @brief Returns runtime statistics snapshot.
73 * @return Allocator stats for this free-list
74 */
75 [[nodiscard]] AllocatorStats Stats() const noexcept;
76
77 /**
78 * @brief Returns configured backing capacity.
79 * @return Capacity in bytes
80 */
81 [[nodiscard]] size_t InitialCapacity() const noexcept { return capacity_; }
82
83 /**
84 * @brief Returns configured backing capacity.
85 * @return Capacity in bytes
86 */
87 [[nodiscard]] size_t TotalCapacity() const noexcept { return capacity_; }
88
89 /**
90 * @brief Returns free-list block count.
91 * @return Number of disjoint free blocks
92 */
93 [[nodiscard]] size_t FreeBlockCount() const noexcept {
94 return free_block_count_.load(std::memory_order_relaxed);
95 }
96
97private:
98 struct FreeBlock {
99 size_t size = 0;
100 FreeBlock* next = nullptr;
101 };
102
103 struct AllocationHeader {
104 size_t size = 0;
105 size_t padding = 0;
106 };
107
108 void Initialize() noexcept;
109 void MoveFrom(FixedFreeListAllocator& other) noexcept;
110 void InsertAndCoalesce(FreeBlock* block) noexcept;
111 void ReleaseUnlocked() noexcept;
112 void Release() noexcept;
113
114 [[nodiscard]] void* do_allocate(size_t bytes, size_t alignment) override;
115 void do_deallocate(void* ptr, size_t bytes, size_t alignment) override;
116
117 [[nodiscard]] bool do_is_equal(
118 const std::pmr::memory_resource& other) const noexcept override {
119 return this == &other;
120 }
121
122 size_t capacity_ = 0;
123 std::byte* buffer_ = nullptr;
124 FreeBlock* free_list_ = nullptr;
125 mutable HELIOS_MEMORY_PROFILE_SHARED_LOCKABLE(std::shared_mutex, mutex_);
126
127 std::atomic<size_t> free_block_count_{0};
128 std::atomic<size_t> used_memory_{0};
129 std::atomic<size_t> peak_usage_{0};
130 std::atomic<size_t> allocation_count_{0};
131 std::atomic<size_t> total_allocations_{0};
132 std::atomic<size_t> total_deallocations_{0};
133 std::atomic<size_t> alignment_waste_{0};
134};
135
137 : capacity_(capacity) {
138 HELIOS_ASSERT(capacity_ >= sizeof(void*) * 4,
139 "capacity '{}' is too small for fixed free-list!", capacity_);
140 buffer_ = static_cast<std::byte*>(
141 AlignedAlloc(kDefaultAlignment, capacity_, false));
142 HELIOS_VERIFY(buffer_ != nullptr, "Failed to allocate fixed free-list!");
143 HELIOS_MEMORY_PROFILE_ALLOC(buffer_, capacity_, "FixedFreeListAllocator");
144 HELIOS_MEMORY_PROFILE_LOCK_NAME(mutex_,
145 std::string_view{"FixedFreeListAllocator"});
146 Initialize();
147}
148
150 FixedFreeListAllocator&& other) noexcept {
151 const std::scoped_lock lock(other.mutex_);
152 MoveFrom(other);
153}
154
156 FixedFreeListAllocator&& other) noexcept {
157 if (this == &other) [[unlikely]] {
158 return *this;
159 }
160
161 const std::scoped_lock lock(mutex_, other.mutex_);
162 ReleaseUnlocked();
163 MoveFrom(other);
164 return *this;
165}
166
167inline void FixedFreeListAllocator::Reset() noexcept {
168 HELIOS_MEMORY_PROFILE_SCOPE_N("helios::mem::FixedFreeListAllocator::Reset");
169 const std::scoped_lock lock(mutex_);
170 HELIOS_MEMORY_PROFILE_LOCK_MARK(mutex_);
171
172 Initialize();
173 peak_usage_.store(0, std::memory_order_relaxed);
174 total_allocations_.store(0, std::memory_order_relaxed);
175 total_deallocations_.store(0, std::memory_order_relaxed);
176}
177
178inline bool FixedFreeListAllocator::Owns(const void* ptr) const noexcept {
179 HELIOS_MEMORY_PROFILE_SCOPE_N("helios::mem::FixedFreeListAllocator::Owns");
180
181 if (ptr == nullptr) [[unlikely]] {
182 return false;
183 }
184
185 const std::shared_lock lock(mutex_);
186 if (buffer_ == nullptr) [[unlikely]] {
187 return false;
188 }
189
190 const auto address = reinterpret_cast<uintptr_t>(ptr);
191 const auto begin = reinterpret_cast<uintptr_t>(buffer_);
192 return address >= begin && address < begin + capacity_;
193}
194
196 return {
197 .total_allocated = used_memory_.load(std::memory_order_relaxed),
198 .peak_usage = peak_usage_.load(std::memory_order_relaxed),
199 .allocation_count = allocation_count_.load(std::memory_order_relaxed),
200 .total_allocations = total_allocations_.load(std::memory_order_relaxed),
201 .total_deallocations =
202 total_deallocations_.load(std::memory_order_relaxed),
203 .alignment_waste = alignment_waste_.load(std::memory_order_relaxed),
204 };
205}
206
207} // 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
bool Owns(const void *ptr) const noexcept
Checks whether pointer belongs to this allocator's backing buffer.
size_t InitialCapacity() const noexcept
Returns configured backing capacity.
void Reset() noexcept
Resets allocator to a single free region.
FixedFreeListAllocator(size_t capacity) noexcept
Constructs fixed free-list with the given backing capacity.
AllocatorStats Stats() const noexcept
Returns runtime statistics snapshot.
size_t FreeBlockCount() const noexcept
Returns free-list block count.
size_t TotalCapacity() const noexcept
Returns configured backing capacity.
FixedFreeListAllocator & operator=(const FixedFreeListAllocator &)=delete
FixedFreeListAllocator(const FixedFreeListAllocator &)=delete
bool Empty() const noexcept
Returns true when no allocations are active.
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