FE 0.15.0
Fast, Effecient FrontEnds
Loading...
Searching...
No Matches
arena.h
Go to the documentation of this file.
1#pragma once
2
3#include <concepts>
4
5#include <algorithm>
6#include <list>
7#include <memory>
8#include <memory_resource>
9#include <new>
10#include <type_traits>
11#include <utility>
12
13#include "fe/assert.h"
14#include "fe/span.h"
15#include "fe/vla.h"
16
17namespace fe {
18
19/// An arena pre-allocates so-called *pages* of size Arena::page_size_.
20/// You can use Arena::allocate to obtain memory from this.
21/// When a page runs out of memory, the next page will be (pre-)allocated.
22/// You cannot directly release memory obtained via this method.
23/// Instead, *all* memory acquired via this Arena will be released as soon as this Arena will be destroyed.
24/// As an exception, you can Arena::deallocate memory that has just been acquired.
25class Arena {
26public:
27 static constexpr size_t Default_Page_Size = 1024 * 1024; ///< 1MB.
28
29 /// A [memory resource](https://en.cppreference.com/w/cpp/memory/memory_resource) bridge in order to use this
30 /// Arena for [pmr containers](https://en.cppreference.com/w/cpp/memory/polymorphic_allocator).
31 /// Access it via Arena::resource.
33 public:
34 explicit MemoryResource(Arena& arena) noexcept
35 : arena_(arena) {}
36
37 private:
38 void* do_allocate(size_t bytes, size_t alignment) override { return arena_.allocate(bytes, alignment); }
39 void do_deallocate(void*, size_t, size_t) override {}
40 bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override {
41 if (this == &other) return true;
42 auto resource = dynamic_cast<const MemoryResource*>(&other);
43 return resource != nullptr && &arena_ == &resource->arena_;
44 }
45
46 Arena& arena_;
47 };
48
49 /// An [allocator](https://en.cppreference.com/w/cpp/named_req/Allocator) in order to use this Arena for
50 /// [containers](https://en.cppreference.com/w/cpp/named_req/AllocatorAwareContainer).
51 /// Construct it via Arena::allocator.
52 template<class T>
53 struct Allocator {
54 using value_type = T;
55
56 Allocator() = delete;
57
58 template<class U>
59 constexpr Allocator(const Arena::Allocator<U>& allocator) noexcept
60 : arena(allocator.arena) {}
61 constexpr Allocator(Arena& arena) noexcept
62 : arena(arena) {}
63
64 [[nodiscard]] T* allocate(size_t num_elems) { return arena.allocate<T>(num_elems); }
65
66 constexpr void deallocate(T*, size_t) noexcept {}
67
68 /// All Arena::Allocator%s compare equal.
69 /// Allocator equality denotes deallocation-compatibility, and Arena::Allocator::deallocate is a no-op,
70 /// so storage from any instance can be "freed" through any other.
71 /// This also keeps allocator-aware container `swap` (e.g. in SymPool::swap) well-defined without requiring
72 /// `propagate_on_container_swap`, which we cannot enable here because `arena` is a non-rebindable reference.
73 // clang-format off
74 template<class U> constexpr bool operator==(const Allocator<U>&) const noexcept { return true; }
75 template<class U> constexpr bool operator!=(const Allocator<U>&) const noexcept { return false; }
76 // clang-format on
77
79 };
80
81 template<class T>
82 struct Deleter {
83 constexpr Deleter() noexcept = default;
84 template<class U, std::enable_if_t<std::is_convertible_v<U*, T*>, int> = 0>
85 constexpr Deleter(const Deleter<U>&) noexcept {}
86
87 constexpr void operator()(T* ptr) const noexcept(noexcept(ptr->~T())) { ptr->~T(); }
88 };
89
90 /// A non-owning pointer into an Arena.
91 /// The Arena outlives it and releases everything at once, so nothing is ever destroyed
92 /// through it - unlike Arena::Ptr, which at least runs the destructor.
93 template<class T>
94 class Ref {
95 public:
96 constexpr Ref() noexcept = default;
97 constexpr Ref(std::nullptr_t) noexcept {}
98 constexpr explicit Ref(T* ptr) noexcept
99 : ptr_(ptr) {}
100 template<class U>
101 requires std::convertible_to<U*, T*> constexpr Ref(Ref<U> ref) noexcept
102 : ptr_(ref.get()) {}
103
104 constexpr T* get() const noexcept { return ptr_; }
105 constexpr T* operator->() const noexcept { return ptr_; }
106 constexpr T& operator*() const noexcept { return *ptr_; }
107 constexpr explicit operator bool() const noexcept { return ptr_ != nullptr; }
108 constexpr bool operator==(const Ref&) const noexcept = default;
109
110 private:
111 T* ptr_ = nullptr;
112 };
113
114 template<class T>
115 using Ptr = std::unique_ptr<T, Deleter<T>>;
116 using State = std::pair<size_t, size_t>;
117
118 /// @name Construction
119 ///@{
120 Arena(const Arena&) = delete;
121 explicit Arena(size_t page_size = Default_Page_Size)
122 : page_size_(page_size) {
123 pages_.emplace_back();
124 }
125 Arena(Arena&& other) noexcept
126 : Arena() {
127 swap(*this, other);
128 }
130
131 /// Create Allocator from Arena.
132 template<class T>
134 return Allocator<T>(*this);
135 }
136
137 std::pmr::memory_resource* resource() noexcept { return &resource_; }
138 const std::pmr::memory_resource* resource() const noexcept { return &resource_; }
139
140 /// This is a [std::unique_ptr](https://en.cppreference.com/w/cpp/memory/unique_ptr)
141 /// that uses the Arena under the hood
142 /// and whose Deleter will *only* invoke the destructor but *not* `delete` anything;
143 /// memory will be released upon destruction of the Arena.
144 ///
145 /// Use like this:
146 /// ```
147 /// auto ptr = arena.mk<Foo>(a, b, c); // new Foo(a, b, c) placed into arena
148 /// ```
149 template<class T, class... Args>
150 Ptr<T> mk(Args&&... args) {
151 return Ptr<T>(create<T>(std::forward<Args>(args)...), Deleter<T>());
152 }
153
154 /// Like Arena::mk, but yields a Ref: nothing will ever destroy the object.
155 /// Use like this:
156 /// ```
157 /// auto ref = arena.ref<Foo>(a, b, c); // new Foo(a, b, c) placed into arena, never destroyed
158 /// ```
159 template<class T, class... Args>
160 Ref<T> ref(Args&&... args) {
161 static_assert(std::is_trivially_destructible_v<std::remove_const_t<T>>,
162 "a Ref never destroys - use Arena::mk for a type with a destructor");
163 return Ref<T>(create<T>(std::forward<Args>(args)...));
164 }
165
166 /// An Arena-allocated copy of @p range.
167 template<std::ranges::input_range R, class T = std::ranges::range_value_t<R>>
168 [[nodiscard]] Span<T> copy(const R& range) {
169 static_assert(std::is_trivially_destructible_v<T>);
170 auto n = std::ranges::size(range);
171 auto ptr = allocate<T>(n);
172 std::uninitialized_copy(std::ranges::begin(range), std::ranges::end(range), ptr);
173 return {ptr, n};
174 }
175 ///@}
176
177 /// @name Allocate
178 ///@{
179
180 /// Get @p n bytes of fresh memory.
181 /// @note When a fresh page is allocated, its base is only aligned to the @p align of the allocation that
182 /// triggered it. A *later* allocation in the same page that requests a *larger* alignment has its offset
183 /// aligned but may still be under-aligned relative to its request. This is a non-issue for the default
184 /// (max-aligned) page size and for arenas with uniform alignment; only tiny custom arenas mixing alignments
185 /// can hit it.
186 [[nodiscard]] void* allocate(size_t num_bytes, size_t align) {
187 if (num_bytes == 0) return nullptr;
188 assert(align != 0);
189
190 auto aligned_index = Arena::align(index_, align);
191 if (aligned_index + num_bytes > pages_.back().size) {
192 pages_.emplace_back(std::max(page_size_, num_bytes), align);
193 aligned_index = 0;
194 }
195
196 auto result = pages_.back().buffer + aligned_index;
197 index_ = aligned_index + num_bytes;
198 return result;
199 }
200
201 template<class T>
202 [[nodiscard]] T* allocate(size_t num_elems) {
203 return static_cast<T*>(allocate(num_elems * sizeof(T), alignof(T)));
204 }
205 ///@}
206
207 /// @name Deallocate
208 /// Deallocate memory again in reverse order.
209 /// Use like this:
210 /// ```
211 /// auto state = arena.state();
212 /// auto ptr = arena.allocate(n);
213 /// if (/* I don't want that */) arena.deallocate(state);
214 /// ```
215 /// @warning Only use, if you really know what you are doing.
216 ///@{
217
218 /// Removes @p num_bytes again.
219 void deallocate(size_t num_bytes) noexcept {
220 assert(num_bytes <= index_);
221 index_ -= num_bytes;
222 }
223 [[nodiscard]] State state() const noexcept { return {pages_.size(), index_}; }
224
225 void deallocate(State state) noexcept {
226 assert(state.first > 0);
227 assert(state.first <= pages_.size());
228 while (pages_.size() > state.first)
229 pages_.pop_back();
230 assert(state.second <= pages_.back().size);
231 index_ = state.second;
232 }
233 ///@}
234
235 friend void swap(Arena& a1, Arena& a2) noexcept {
236 using std::swap;
237 // clang-format off
238 swap(a1.pages_, a2.pages_);
239 swap(a1.page_size_, a2.page_size_);
240 swap(a1.index_, a2.index_);
241 // clang-format on
242 }
243
244 /// Align @p i to @p a.
245 static constexpr size_t align(size_t i, size_t a) noexcept { return (i + (a - 1)) & ~(a - 1); }
246
247private:
248 /// Placement-new%s a `T`, allocating and filling its fe::VLA%s, if it has any.
249 template<class T, class... Args>
250 std::remove_const_t<T>* create(Args&&... args) {
251 using U = std::remove_const_t<T>;
252 if constexpr (VLAed<U>) {
253 static_assert(sizeof...(Args) >= U::num_vlas(), "one range per VLA, as the last arguments");
254 constexpr auto n = sizeof...(Args) - U::num_vlas();
255 return create_vla<U>(std::make_index_sequence<n>(), std::make_index_sequence<U::num_vlas()>(),
256 std::forward_as_tuple(std::forward<Args>(args)...));
257 } else {
258 static_assert(
259 !requires { typename U::VLA_Self; },
260 "this inherits the fe::VLA of a base class, so its arrays would sit at that base's "
261 "offset - only the most derived class may declare VLA_Types");
262 return new (allocate<U>(1)) U(std::forward<Args>(args)...);
263 }
264 }
265
266 template<class U, size_t... Hs, size_t... Ts, class Tuple>
267 U* create_vla(std::index_sequence<Hs...>, std::index_sequence<Ts...>, Tuple&& tuple) {
268 auto counts = std::array<size_t, sizeof...(Ts)>{std::ranges::size(std::get<sizeof...(Hs) + Ts>(tuple))...};
269 auto align = std::max(alignof(U), U::vla_align());
270 auto ptr = new (allocate(U::vla_bytes(counts), align)) U(std::get<Hs>(std::forward<Tuple>(tuple))...);
271 ptr->fill_vla(std::get<sizeof...(Hs) + Ts>(tuple)...);
272 return ptr;
273 }
274
275 Arena& align(size_t a) noexcept { return index_ = align(index_, a), *this; }
276
277 struct Page {
278 constexpr Page() noexcept = default;
279 Page(size_t size, size_t align)
280 : size(size)
281 , align(align)
282 , buffer((char*)::operator new[](size, std::align_val_t(align))) {}
283 constexpr ~Page() noexcept {
284 if (buffer) ::operator delete[](buffer, std::align_val_t(align));
285 }
286
287 const size_t size = 0;
288 const size_t align = 0;
289 char* buffer = nullptr;
290 };
291
292 std::list<Page> pages_;
293 size_t page_size_;
294 size_t index_ = 0;
295 MemoryResource resource_{*this};
296};
297
298} // namespace fe
A memory resource bridge in order to use this Arena for pmr containers.
Definition arena.h:32
bool do_is_equal(const std::pmr::memory_resource &other) const noexcept override
Definition arena.h:40
MemoryResource(Arena &arena) noexcept
Definition arena.h:34
void do_deallocate(void *, size_t, size_t) override
Definition arena.h:39
void * do_allocate(size_t bytes, size_t alignment) override
Definition arena.h:38
A non-owning pointer into an Arena.
Definition arena.h:94
constexpr bool operator==(const Ref &) const noexcept=default
constexpr Ref() noexcept=default
constexpr Ref(T *ptr) noexcept
Definition arena.h:98
constexpr T * operator->() const noexcept
Definition arena.h:105
constexpr T & operator*() const noexcept
Definition arena.h:106
constexpr T * get() const noexcept
Definition arena.h:104
constexpr Ref(Ref< U > ref) noexcept
Definition arena.h:101
void * allocate(size_t num_bytes, size_t align)
Get n bytes of fresh memory.
Definition arena.h:186
void deallocate(State state) noexcept
Definition arena.h:225
Ref< T > ref(Args &&... args)
Like Arena::mk, but yields a Ref: nothing will ever destroy the object.
Definition arena.h:160
Arena(const Arena &)=delete
Arena(Arena &&other) noexcept
Definition arena.h:125
Span< T > copy(const R &range)
An Arena-allocated copy of range.
Definition arena.h:168
static constexpr size_t align(size_t i, size_t a) noexcept
Align i to a.
Definition arena.h:245
void deallocate(size_t num_bytes) noexcept
Removes num_bytes again.
Definition arena.h:219
std::pmr::memory_resource * resource() noexcept
Definition arena.h:137
std::pair< size_t, size_t > State
Definition arena.h:116
Arena(size_t page_size=Default_Page_Size)
Definition arena.h:121
T * allocate(size_t num_elems)
Definition arena.h:202
Allocator< T > allocator() noexcept
Create Allocator from Arena.
Definition arena.h:133
friend void swap(Arena &a1, Arena &a2) noexcept
Definition arena.h:235
Arena & operator=(Arena)=delete
std::unique_ptr< T, Deleter< T > > Ptr
Definition arena.h:115
const std::pmr::memory_resource * resource() const noexcept
Definition arena.h:138
Ptr< T > mk(Args &&... args)
This is a std::unique_ptr that uses the Arena under the hood and whose Deleter will only invoke the d...
Definition arena.h:150
static constexpr size_t Default_Page_Size
1MB.
Definition arena.h:27
State state() const noexcept
Definition arena.h:223
This is a thin wrapper for std::span<T, N> with the following additional features:
Definition span.h:33
Does T carry VLAs?
Definition vla.h:124
Definition algo.h:17
Definition span.h:150
An allocator in order to use this Arena for containers.
Definition arena.h:53
constexpr bool operator!=(const Allocator< U > &) const noexcept
Definition arena.h:75
constexpr bool operator==(const Allocator< U > &) const noexcept
All Arena::Allocators compare equal.
Definition arena.h:74
T * allocate(size_t num_elems)
Definition arena.h:64
constexpr void deallocate(T *, size_t) noexcept
Definition arena.h:66
constexpr Allocator(const Arena::Allocator< U > &allocator) noexcept
Definition arena.h:59
constexpr Allocator(Arena &arena) noexcept
Definition arena.h:61
constexpr Deleter() noexcept=default
constexpr void operator()(T *ptr) const noexcept(noexcept(ptr->~T()))
Definition arena.h:87