Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/complexity.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
| 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) |

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

Expand Down
13 changes: 6 additions & 7 deletions docs/containers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
49 changes: 30 additions & 19 deletions src/libmem/multislab.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -156,27 +156,16 @@ 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);

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));
}

/* ========================================================================
Expand Down Expand Up @@ -285,11 +274,7 @@ public:
return iterator{};
}

const auto base{reinterpret_cast<std::uintptr_t>(node->raw_memory)};
const auto p{reinterpret_cast<std::uintptr_t>(ptr)};
const auto index{static_cast<std::uint32_t>((p - base) / BlockSize)};

return iterator_at(node, index);
return iterator_at(node, node->allocator.index_of(ptr));
}

/**
Expand Down Expand Up @@ -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
* ======================================================================== */
Expand Down Expand Up @@ -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)}; }

Expand Down
7 changes: 3 additions & 4 deletions src/libmem/pool.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ public:
try {
::new (alloc.ptr) T(std::forward<Args>(args)...);
} catch (...) {
pool_.deallocate(alloc.ptr);
pool_.deallocate(alloc.it);
throw;
}
}
Expand Down Expand Up @@ -393,9 +393,8 @@ public:
pool_iterator next{inner};
++next;

auto* raw{*inner};
static_cast<T*>(raw)->~T();
pool_.deallocate(raw);
static_cast<T*>(*inner)->~T();
pool_.deallocate(inner);
--size_;

return iterator{next};
Expand Down
27 changes: 20 additions & 7 deletions src/libmem/slab.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::uint32_t>((reinterpret_cast<std::uintptr_t>(ptr) - reinterpret_cast<std::uintptr_t>(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<std::uintptr_t>(ptr)};
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<std::size_t>(hit - bitmap_.begin()), *hit);
}

constexpr std::uint32_t ptr_to_index(const void* ptr) const noexcept {
return static_cast<std::uint32_t>((reinterpret_cast<std::uintptr_t>(ptr) - reinterpret_cast<std::uintptr_t>(memory_)) / BlockSize);
}
};

/* Verify slab::iterator satisfies input_iterator. */
Expand Down
42 changes: 42 additions & 0 deletions tests/src/multislab_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<block, per_slab> by_ptr{policy};
multislab<block, per_slab> by_it{policy};

std::vector<void*> ptrs(3 * per_slab);
std::vector<void*> 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<block, 4> 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);
}
28 changes: 28 additions & 0 deletions tests/src/pool_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <algorithm>
#include <cstdint>
#include <ranges>
#include <stdexcept>
#include <vector>

import libmem;
Expand Down Expand Up @@ -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<throws_on_negative, 4> 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);
}
15 changes: 15 additions & 0 deletions tests/src/slab_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,18 @@ TEST(SlabTest, iterator_crosses_empty_words) {
}
EXPECT_EQ(visited, (std::vector<void*>{blocks[0], blocks[130]}));
}

TEST(SlabTest, index_of_and_deallocate_at_round_trip) {
alignas(block) std::array<std::byte, block * 8> storage{};
slab<block, 8> 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);
}
Loading