Skip to content

Commit 9d7b00b

Browse files
author
Conor
committed
slab alloc
1 parent b0c82bb commit 9d7b00b

3 files changed

Lines changed: 245 additions & 0 deletions

File tree

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ target_sources(libfork_libfork
9191
src/batteries/contexts.cxx
9292
src/batteries/geometric_stack.cxx
9393
src/batteries/adaptor_stack.cxx
94+
src/batteries/slab_stack.cxx
9495
src/batteries/dummy_stack.cxx
9596
# libfork.schedulers
9697
src/schedulers/schedulers.cxx

src/batteries/batteries.cxx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export module libfork.batteries;
33
export import :deque;
44
export import :geometric_stack;
55
export import :adaptor_stack;
6+
export import :slab_stack;
67
export import :dummy_stack;
78
export import :adaptors;
89
export import :contexts;

src/batteries/slab_stack.cxx

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
module;
2+
#include "libfork/__impl/assume.hpp"
3+
#include "libfork/__impl/compiler.hpp"
4+
#include "libfork/__impl/exception.hpp"
5+
export module libfork.batteries:slab_stack;
6+
7+
import std;
8+
9+
import libfork.utils;
10+
11+
namespace lf {
12+
13+
/**
14+
* @brief A slab_stack is a user-space stack backed by a single fixed-size slab of memory.
15+
*
16+
* The ctrl metadata and usable stack space are fused into a single allocation: a header
17+
* at the front of the slab is followed immediately by the usable nodes. There is no
18+
* segmentation, caching, or geometric growth — if the slab is full, push throws.
19+
*
20+
* For this to conform to `worker_stack` the allocators void pointer type must be `void *`
21+
*/
22+
export template <allocator_of<std::byte> Allocator = std::allocator<std::byte>>
23+
class slab_stack {
24+
25+
// Alignment unit — all allocations are a multiple of this size.
26+
struct alignas(k_new_align) node {};
27+
static_assert(sizeof(node) == k_new_align);
28+
29+
using node_traits = std::allocator_traits<Allocator>::template rebind_traits<node>;
30+
using node_alloc_t = node_traits::allocator_type;
31+
using node_ptr = node_traits::pointer;
32+
using void_ptr = node_traits::void_pointer;
33+
using size_int = node_traits::size_type;
34+
using diff_int = node_traits::difference_type;
35+
36+
// Fused ctrl+node header — lives at the very start of every slab allocation.
37+
// The usable stack space (size nodes) follows directly after the header region.
38+
struct slab {
39+
[[no_unique_address]]
40+
node_alloc_t node_alloc; // Propagated to new owners on acquire.
41+
node_ptr sp_cache; // Stack pointer saved across release/acquire.
42+
diff_int size; // Usable node count in this slab.
43+
};
44+
45+
// Number of node-sized units occupied by the header at the front of each allocation.
46+
static constexpr diff_int k_header_nodes =
47+
safe_cast<diff_int>((sizeof(slab) + sizeof(node) - 1) / sizeof(node));
48+
49+
// Default capacity: fill one page minus the header.
50+
static constexpr diff_int k_default_nodes =
51+
safe_cast<diff_int>(k_page_size / sizeof(node)) - k_header_nodes;
52+
53+
static_assert(k_default_nodes > 0);
54+
55+
struct release_t {
56+
explicit constexpr release_t(key_t) noexcept {}
57+
};
58+
59+
class checkpoint_t {
60+
public:
61+
constexpr checkpoint_t() noexcept = default;
62+
constexpr auto operator==(checkpoint_t const &) const noexcept -> bool = default;
63+
64+
private:
65+
friend slab_stack;
66+
explicit constexpr checkpoint_t(slab *ptr) noexcept : m_slab(ptr) {}
67+
slab *m_slab = nullptr;
68+
};
69+
70+
public:
71+
constexpr slab_stack() : slab_stack(Allocator{}) {}
72+
explicit constexpr slab_stack(Allocator const &alloc, diff_int num_nodes = k_default_nodes)
73+
: m_alloc(alloc) {
74+
init_slab(num_nodes);
75+
}
76+
77+
constexpr slab_stack(slab_stack const &) = delete;
78+
constexpr slab_stack(slab_stack &&) = delete;
79+
80+
constexpr auto operator=(slab_stack const &) -> slab_stack & = delete;
81+
constexpr auto operator=(slab_stack &&) -> slab_stack & = delete;
82+
83+
constexpr ~slab_stack() noexcept {
84+
LF_ASSUME(empty());
85+
free_slab(m_slab);
86+
}
87+
88+
/**
89+
* @brief Test if the stack is empty (all pushes have been popped).
90+
*/
91+
[[nodiscard]]
92+
constexpr auto empty() const noexcept -> bool {
93+
return m_sp == m_lo;
94+
}
95+
96+
/**
97+
* @brief Get a checkpoint of the stack for transfer to another stack instance.
98+
*/
99+
[[nodiscard]]
100+
constexpr auto checkpoint() noexcept -> checkpoint_t {
101+
return checkpoint_t{m_slab};
102+
}
103+
104+
/**
105+
* @brief Allocate size bytes on the stack and return a pointer to the base of the allocation.
106+
*/
107+
[[nodiscard]]
108+
constexpr auto push(std::size_t size) -> void_ptr {
109+
LF_ASSUME(size > 0);
110+
111+
constexpr diff_int node_size = sizeof(node);
112+
113+
diff_int push_bytes = safe_cast<diff_int>(round_to_multiple<sizeof(node)>(size));
114+
115+
LF_ASSUME(push_bytes >= node_size);
116+
LF_ASSUME(push_bytes % node_size == 0);
117+
118+
// Optimized to just the subtraction because multiplication cancels the implicit division.
119+
diff_int free_bytes = node_size * (m_hi - m_sp);
120+
121+
if (push_bytes > free_bytes) [[unlikely]] {
122+
slab_full();
123+
}
124+
125+
diff_int num_nodes = push_bytes / node_size;
126+
127+
// node_ptr -> void_ptr
128+
return static_cast<void_ptr>(std::exchange(m_sp, m_sp + num_nodes));
129+
}
130+
131+
/**
132+
* @brief Deallocate the most recent allocation of n bytes at ptr.
133+
*/
134+
constexpr void pop(void_ptr ptr, [[maybe_unused]] std::size_t n) noexcept {
135+
LF_ASSUME(!empty());
136+
LF_ASSUME(m_sp != nullptr);
137+
LF_ASSUME(ptr != nullptr);
138+
139+
// Inverse of push: void_ptr -> node_ptr
140+
m_sp = static_cast<node_ptr>(ptr);
141+
}
142+
143+
[[nodiscard]]
144+
constexpr auto prepare_release() const noexcept -> release_t {
145+
// Guard against null release (failed prior allocation).
146+
if (m_slab != nullptr) {
147+
m_slab->sp_cache = m_sp;
148+
}
149+
return release_t{key()};
150+
}
151+
152+
constexpr void release([[maybe_unused]] release_t) noexcept {
153+
diff_int next_size = (m_slab != nullptr) ? m_slab->size : k_default_nodes;
154+
155+
// Hand off the current slab to whoever holds the checkpoint; clear local state.
156+
m_slab = nullptr;
157+
m_lo = nullptr;
158+
m_sp = nullptr;
159+
m_hi = nullptr;
160+
161+
// Pre-allocate a fresh slab for our next use. If this throws, swallow the
162+
// exception — push will see no space (m_hi - m_sp == 0) and throw instead.
163+
LF_TRY {
164+
init_slab(next_size);
165+
} LF_CATCH_ALL {
166+
}
167+
}
168+
169+
constexpr void acquire(checkpoint_t ckpt) noexcept {
170+
LF_ASSUME(empty());
171+
172+
if (ckpt.m_slab == nullptr) {
173+
return;
174+
}
175+
176+
// Discard the fresh empty slab we prepared during release() (may be null on alloc failure).
177+
free_slab(m_slab);
178+
179+
m_slab = ckpt.m_slab;
180+
181+
if constexpr (!node_traits::is_always_equal::value) {
182+
m_alloc = node_alloc_t{std::as_const(m_slab->node_alloc)};
183+
}
184+
185+
LF_ASSUME(m_slab != nullptr);
186+
187+
load_local();
188+
}
189+
190+
private:
191+
[[no_unique_address]]
192+
node_alloc_t m_alloc;
193+
194+
slab *m_slab = nullptr;
195+
node_ptr m_lo = nullptr; // Base of usable space in the current slab.
196+
node_ptr m_sp = nullptr; // Stack pointer for the current slab.
197+
node_ptr m_hi = nullptr; // One-past-the-end of usable space in the current slab.
198+
199+
// Restore local pointers from the slab header, taking sp from the cache.
200+
constexpr void load_local() noexcept {
201+
LF_ASSUME(m_slab != nullptr);
202+
node_ptr base = reinterpret_cast<node_ptr>(m_slab) + k_header_nodes;
203+
m_lo = base;
204+
m_hi = base + m_slab->size;
205+
m_sp = m_slab->sp_cache;
206+
}
207+
208+
// Allocate and construct a fresh slab with num_nodes usable nodes.
209+
constexpr void init_slab(diff_int num_nodes) {
210+
LF_ASSUME(num_nodes > 0);
211+
212+
size_int total = safe_cast<size_int>(k_header_nodes + num_nodes);
213+
node_ptr raw = node_traits::allocate(m_alloc, total);
214+
215+
LF_TRY {
216+
m_slab = std::construct_at(reinterpret_cast<slab *>(std::to_address(raw)), m_alloc, nullptr, num_nodes);
217+
} LF_CATCH_ALL {
218+
node_traits::deallocate(m_alloc, raw, total);
219+
LF_RETHROW;
220+
}
221+
222+
node_ptr base = raw + k_header_nodes;
223+
m_lo = m_sp = base;
224+
m_hi = base + num_nodes;
225+
}
226+
227+
// Destroy and deallocate a slab (no-op if null).
228+
constexpr void free_slab(slab *s) noexcept {
229+
if (s != nullptr) {
230+
size_int total = safe_cast<size_int>(k_header_nodes + s->size);
231+
node_ptr raw = reinterpret_cast<node_ptr>(s);
232+
std::destroy_at(s);
233+
node_traits::deallocate(m_alloc, raw, total);
234+
}
235+
}
236+
237+
[[noreturn]]
238+
static void slab_full() {
239+
LF_THROW(std::bad_alloc{});
240+
}
241+
};
242+
243+
} // namespace lf

0 commit comments

Comments
 (0)