diff --git a/docs/complexity.md b/docs/complexity.md index e9f70ed..ef53285 100644 --- a/docs/complexity.md +++ b/docs/complexity.md @@ -5,7 +5,7 @@ | Operation | `slab` | `multislab` | `arena` / `typed_arena` | `pool` | |-----------|--------|-------------|-------------------------|--------| | allocate | O(N/64) | O(N/64) amortised | O(1) | O(N/64) amortised | -| deallocate | O(1) | O(S) | no-op | O(S) | +| deallocate | O(1) | O(1) by iterator, O(S) by pointer | no-op | O(1) (`erase`) | | iterate (skip empties) | O(N/64) per word | O(N/64) per word | n/a | O(N/64) per word | | reset / clear | O(W) | O(S) | O(1) / O(D) | O(S + E) | @@ -13,7 +13,7 @@ N = capacity in blocks, W = bitmap words (N/64), S = number of slab pages, D = registered destructors, E = live elements (for non-trivial destructors). The O(S) terms are all `find_owner`, a linear scan over slab pages. See -[containers.md](containers.md#allocators) for why allocation does not pay it. +[containers.md](containers.md#allocators) for which calls pay it. ## Sequences diff --git a/docs/containers.md b/docs/containers.md index 22d0546..a1ae74f 100644 --- a/docs/containers.md +++ b/docs/containers.md @@ -197,13 +197,12 @@ is move-only. Planned: `freelist`, an intrusive free-list allocator for variable-size blocks. -`find_owner` is a linear scan over slab pages, and is what makes `deallocate` O(S) -on `multislab` and `pool`. Allocation avoids it entirely: `slab::allocate_at()` and -`multislab::allocate_at()` return the block *plus* its position, both of which the -allocator already had in hand. `pool::emplace` uses this, so insertion stays -O(N/64) amortised rather than paying an O(S) scan to recover a position it just -discarded. Reach for `make_iterator(ptr)` only when you have a pointer and no -allocation to go with it. +`find_owner` is a linear scan over slab pages, paid by `multislab::deallocate(ptr)` +and `make_iterator(ptr)`. Everything that already holds a position skips it: +`slab::allocate_at()` and `multislab::allocate_at()` return the block *plus* its +position, and `multislab::deallocate(iterator)` releases through it. `pool` uses +both, so `emplace` stays O(N/64) amortised and `erase` is O(1). Reach for the +pointer overloads only when you have a pointer and no position to go with it. ## Constant evaluation diff --git a/src/libmem/multislab.cppm b/src/libmem/multislab.cppm index 423d328..ceed3a1 100644 --- a/src/libmem/multislab.cppm +++ b/src/libmem/multislab.cppm @@ -156,6 +156,8 @@ public: /** * @brief Release a block previously obtained from `allocate()`. * @pre `ptr` was allocated from this multislab and has not been double-freed. + * @note O(S): finds the owning slab by scanning. Prefer `deallocate(iterator)` + * when a position is at hand. */ void deallocate(void* ptr) noexcept { assert(ptr != nullptr); @@ -163,20 +165,7 @@ public: node_type* node{find_owner(ptr)}; assert(node != nullptr && "pointer not owned by this allocator"); - if (node->full()) [[unlikely]] { - move_to_active(node); - } - - node->allocator.deallocate(ptr); - node->used--; - - /* Became empty: apply shrink policy. */ - if (node->used == 0) [[unlikely]] { - empty_count_++; - if (policy_.should_shrink(empty_count_, slab_count_)) { - unlink_and_free(node); - } - } + release(node, node->allocator.index_of(ptr)); } /* ======================================================================== @@ -285,11 +274,7 @@ public: return iterator{}; } - const auto base{reinterpret_cast(node->raw_memory)}; - const auto p{reinterpret_cast(ptr)}; - const auto index{static_cast((p - base) / BlockSize)}; - - return iterator_at(node, index); + return iterator_at(node, node->allocator.index_of(ptr)); } /** @@ -320,6 +305,15 @@ public: return {raw.ptr, iterator_at(raw.node, raw.index)}; } + /** + * @brief Release the block `pos` points at, in O(1). + * @pre `pos` is dereferenceable. Iterators to other blocks stay valid. + */ + void deallocate(const iterator& pos) noexcept { + assert(pos != end()); + release(pos.node_, pos.slab_iter_.index()); + } + /* ======================================================================== * Destroy * ======================================================================== */ @@ -375,6 +369,23 @@ private: return {alloc.ptr, node, alloc.index}; } + void release(node_type* node, const std::uint32_t index) noexcept { + if (node->full()) [[unlikely]] { + move_to_active(node); + } + + node->allocator.deallocate_at(index); + node->used--; + + /* Became empty: apply shrink policy. */ + if (node->used == 0) [[unlikely]] { + empty_count_++; + if (policy_.should_shrink(empty_count_, slab_count_)) { + unlink_and_free(node); + } + } + } + /** @brief Build an iterator at `index` within `node`. */ iterator iterator_at(node_type* node, const std::uint32_t index) const noexcept { return iterator{node, node->allocator.make_iterator(index)}; } diff --git a/src/libmem/pool.cppm b/src/libmem/pool.cppm index 7c32dcb..dd66f1e 100644 --- a/src/libmem/pool.cppm +++ b/src/libmem/pool.cppm @@ -332,7 +332,7 @@ public: try { ::new (alloc.ptr) T(std::forward(args)...); } catch (...) { - pool_.deallocate(alloc.ptr); + pool_.deallocate(alloc.it); throw; } } @@ -393,9 +393,8 @@ public: pool_iterator next{inner}; ++next; - auto* raw{*inner}; - static_cast(raw)->~T(); - pool_.deallocate(raw); + static_cast(*inner)->~T(); + pool_.deallocate(inner); --size_; return iterator{next}; diff --git a/src/libmem/slab.cppm b/src/libmem/slab.cppm index bec26c2..aad2b0a 100644 --- a/src/libmem/slab.cppm +++ b/src/libmem/slab.cppm @@ -166,14 +166,28 @@ public: */ constexpr void deallocate(void* ptr) noexcept { assert(ptr != nullptr); - assert(owns(ptr)); + deallocate_at(index_of(ptr)); + } - const auto index{ptr_to_index(ptr)}; + /** + * @brief Release the block at bit-index `index`, as reported by `allocate_at()` or an iterator. + * @pre The block at `index` is allocated. + */ + constexpr void deallocate_at(const std::uint32_t index) noexcept { + assert(index < block_count_); assert(detail::bitmap_test(bitmap_, index) && "double-free detected"); - detail::bitmap_clear(bitmap_, index); } + /** + * @brief Bit-index of the block at `ptr`. + * @pre `owns(ptr)`. + */ + constexpr std::uint32_t index_of(const void* ptr) const noexcept { + assert(owns(ptr)); + return static_cast((reinterpret_cast(ptr) - reinterpret_cast(memory_)) / BlockSize); + } + /** @brief Test whether `ptr` belongs to this slab's memory region. */ constexpr bool owns(const void* ptr) const noexcept { const auto p{reinterpret_cast(ptr)}; @@ -244,6 +258,9 @@ public: constexpr void* operator*() const noexcept { return slab_->index_to_ptr(index_); } + /** @brief Bit-index of the current block; `capacity` at the end. */ + constexpr std::uint32_t index() const noexcept { return index_; } + constexpr iterator& operator++() noexcept { index_ = slab_->next_allocated(index_ + 1); return *this; @@ -310,10 +327,6 @@ private: const auto hit{std::ranges::find_if(later, [](const std::uint64_t word) { return word != 0; })}; return hit == bitmap_.end() ? capacity : first_in(static_cast(hit - bitmap_.begin()), *hit); } - - constexpr std::uint32_t ptr_to_index(const void* ptr) const noexcept { - return static_cast((reinterpret_cast(ptr) - reinterpret_cast(memory_)) / BlockSize); - } }; /* Verify slab::iterator satisfies input_iterator. */ diff --git a/tests/src/multislab_test.cpp b/tests/src/multislab_test.cpp index 7944db5..729f4bf 100644 --- a/tests/src/multislab_test.cpp +++ b/tests/src/multislab_test.cpp @@ -467,3 +467,45 @@ TEST(MultislabTest, allocation_reuses_a_free_slot_before_growing) { } } } + +/* + * deallocate(iterator) skips find_owner but must leave the same state as + * deallocate(ptr): full->active moves, empty counts, and hysteresis frees. + */ +TEST(MultislabTest, deallocate_by_iterator_matches_deallocate_by_pointer) { + constexpr std::uint32_t per_slab{4}; + const threshold_policy policy{.max_empty_reserve = 1}; + multislab by_ptr{policy}; + multislab by_it{policy}; + + std::vector ptrs(3 * per_slab); + std::vector its(3 * per_slab); + for (std::size_t i{0}; i < ptrs.size(); ++i) { + ptrs[i] = by_ptr.allocate(); + its[i] = by_it.allocate(); + } + + /* Empty the middle slab first, then the others, so both a kept and a + * released empty slab are exercised. */ + for (const std::size_t i : {4u, 5u, 6u, 7u, 0u, 1u, 2u, 3u, 8u, 9u, 10u, 11u}) { + by_ptr.deallocate(ptrs[i]); + by_it.deallocate(by_it.make_iterator(its[i])); + EXPECT_EQ(by_it.slab_count(), by_ptr.slab_count()) << "after " << i; + EXPECT_EQ(by_it.empty_slab_count(), by_ptr.empty_slab_count()) << "after " << i; + EXPECT_EQ(std::ranges::distance(by_it.begin(), by_it.end()), std::ranges::distance(by_ptr.begin(), by_ptr.end())); + } +} + +TEST(MultislabTest, deallocate_through_the_allocate_at_iterator) { + multislab ms{}; + + const auto kept{ms.allocate_at()}; + const auto dropped{ms.allocate_at()}; + ASSERT_NE(dropped.ptr, nullptr); + + ms.deallocate(dropped.it); + EXPECT_EQ(std::ranges::distance(ms.begin(), ms.end()), 1); + EXPECT_EQ(*ms.begin(), kept.ptr); + ms.deallocate(kept.it); + EXPECT_EQ(ms.empty_slab_count(), 1u); +} diff --git a/tests/src/pool_test.cpp b/tests/src/pool_test.cpp index 1e6fb6b..71de8ac 100644 --- a/tests/src/pool_test.cpp +++ b/tests/src/pool_test.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include import libmem; @@ -318,3 +319,30 @@ TEST(PoolTest, erase_while_iterating_visits_every_element_once) { EXPECT_EQ(p.size(), 6u); EXPECT_TRUE(std::ranges::none_of(p, [](const std::int32_t v) { return v % 2 == 0; })); } + +namespace { + +struct throws_on_negative { + std::int32_t v; + explicit throws_on_negative(const std::int32_t x) : v{x} { + if (x < 0) { + throw std::invalid_argument{"negative"}; + } + } +}; + +} // namespace + +/* A throwing constructor gives its slot back through the rollback path. */ +TEST(PoolTest, emplace_rollback_releases_the_slot) { + pool p{}; + p.emplace(1); + EXPECT_THROW(p.emplace(-1), std::invalid_argument); + EXPECT_EQ(p.size(), 1u); + EXPECT_EQ(std::ranges::distance(p.begin(), p.end()), 1); + + const auto reused{p.emplace(2)}; + EXPECT_EQ(std::ranges::distance(p.begin(), p.end()), 2); + EXPECT_EQ(reused->v, 2); + EXPECT_EQ(p.slab_count(), 1u); +} diff --git a/tests/src/slab_test.cpp b/tests/src/slab_test.cpp index 4d24dea..049149b 100644 --- a/tests/src/slab_test.cpp +++ b/tests/src/slab_test.cpp @@ -205,3 +205,18 @@ TEST(SlabTest, iterator_crosses_empty_words) { } EXPECT_EQ(visited, (std::vector{blocks[0], blocks[130]})); } + +TEST(SlabTest, index_of_and_deallocate_at_round_trip) { + alignas(block) std::array storage{}; + slab s{storage.data(), storage.size()}; + + const auto first{s.allocate_at()}; + const auto second{s.allocate_at()}; + EXPECT_EQ(s.index_of(second.ptr), second.index); + EXPECT_EQ(s.begin().index(), first.index); + + s.deallocate_at(first.index); + EXPECT_EQ(s.used_count(), 1u); + EXPECT_EQ(*s.begin(), second.ptr); + EXPECT_EQ(s.allocate_at().index, first.index); +}