|
| 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