blob: cc4ef5fddb312a52dcbe2cd6f970e1fa72a1382d [file] [edit]
// Copyright 2026 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef QUICHE_COMMON_ABIDING_OBJECT_POOL_H_
#define QUICHE_COMMON_ABIDING_OBJECT_POOL_H_
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <new>
#include <type_traits>
#include <utility>
#include "absl/base/config.h"
#include "quiche/common/platform/api/quiche_export.h"
#include "quiche/common/platform/api/quiche_logging.h"
#ifdef ABSL_HAVE_ADDRESS_SANITIZER
#include <sanitizer/asan_interface.h>
#endif
namespace quiche {
// A cache-friendly, thread-local slab object pool with automatic memory
// reclamation and compaction over time.
//
// Slabs are naturally power-of-two aligned for O(1) pointer-to-slab resolution.
// Allocations prioritize the most full non-full slab to concentrate object
// density, allowing emptier slabs to drain to zero and be released to the heap.
// Maintains 1 spare empty slab in reserve to prevent allocation hysteresis
// on slab boundaries.
template <typename T, size_t SlabSizeBytes = 64 * 1024>
class QUICHE_NO_EXPORT AbidingObjectPool {
static_assert((SlabSizeBytes & (SlabSizeBytes - 1)) == 0,
"Slab size must be a power of 2");
public:
struct Deleter {
AbidingObjectPool* pool = nullptr;
template <typename U>
void operator()(U* ptr) const noexcept {
if (ptr == nullptr) {
return;
}
ptr->~U();
QUICHE_DCHECK(pool != nullptr);
if (pool != nullptr) {
pool->Deallocate(static_cast<void*>(ptr));
}
}
};
template <typename U = T>
using Ptr = std::unique_ptr<U, Deleter>;
constexpr AbidingObjectPool() = default;
explicit AbidingObjectPool(size_t initial_slabs) {
for (size_t i = 0; i < initial_slabs; ++i) {
Slab* slab = CreateSlab();
PushFront(non_full_slabs_, slab);
}
}
~AbidingObjectPool() {
while (non_full_slabs_ != nullptr) {
Slab* slab = non_full_slabs_;
RemoveFromList(non_full_slabs_, slab);
DestroySlab(slab);
}
while (full_slabs_ != nullptr) {
Slab* slab = full_slabs_;
RemoveFromList(full_slabs_, slab);
DestroySlab(slab);
}
if (spare_empty_slab_ != nullptr) {
DestroySlab(spare_empty_slab_);
spare_empty_slab_ = nullptr;
}
}
// Non-copyable and non-movable: Outstanding Ptr instances hold a raw pointer
// to `this` inside their Deleter, so the pool must remain pinned in memory.
AbidingObjectPool(const AbidingObjectPool&) = delete;
AbidingObjectPool& operator=(const AbidingObjectPool&) = delete;
AbidingObjectPool(AbidingObjectPool&&) = delete;
AbidingObjectPool& operator=(AbidingObjectPool&&) = delete;
// Constructs an instance of Derived (defaults to T) using pooled memory.
template <typename Derived = T, typename... Args>
Ptr<Derived> Create(Args&&... args) {
static_assert(std::is_same_v<T, Derived> || std::is_base_of_v<T, Derived>,
"Derived must inherit from or be T");
static_assert(sizeof(Derived) <= kSlotSize,
"Derived exceeds allocated slot size");
static_assert(alignof(Derived) <= kAlignment,
"Derived exceeds alignment constraint");
void* slot = Allocate();
Derived* object = ::new (slot) Derived(std::forward<Args>(args)...);
return Ptr<Derived>(object, Deleter{this});
}
size_t total_capacity() const noexcept {
return allocated_slabs_count_ * kSlotsPerSlab;
}
size_t allocated_slabs() const noexcept { return allocated_slabs_count_; }
size_t num_free_slots() const noexcept { return total_free_slots_; }
bool empty() const noexcept { return total_free_slots_ == total_capacity(); }
private:
// Intrusive node overlaid on unallocated slot memory.
struct FreeNode {
// If non-nullptr, points to the next free slot within the Slab.
FreeNode* next;
};
struct Slab {
// Pointers to the next/previous elements in the doubly-linked list.
Slab* prev = nullptr;
Slab* next = nullptr;
// If non-nullptr, points to the first free slot within the Slab.
FreeNode* free_head = nullptr;
// The number of slots that have been handed out so far within this Slab.
size_t allocated_high_watermark = 0;
// The number of slots currently occupied by live objects.
size_t live_objects = 0;
};
// Compile-time assertions and layout constants.
// Disallow small objects; it would be space-inefficient.
static_assert(sizeof(T) >= sizeof(FreeNode),
"Objects stored in AbidingObjectPool should be at least the "
"size of a pointer.");
// Allocations must be aligned at least with `FreeNode`.
static constexpr size_t kAlignment = std::max(alignof(T), alignof(FreeNode));
// If the type to store is smaller than a pointer, this data structure will
// waste memory.
static constexpr size_t kRawSlotSize = sizeof(T);
// Pads slot size to preserve alignment across consecutive array elements.
static constexpr size_t kSlotSize =
(kRawSlotSize + kAlignment - 1) & ~(kAlignment - 1);
// The first slot is after the slab header.
static constexpr size_t kFirstSlotOffset =
(sizeof(Slab) + kAlignment - 1) & ~(kAlignment - 1);
static constexpr size_t kSlotsPerSlab =
(SlabSizeBytes > kFirstSlotOffset)
? (SlabSizeBytes - kFirstSlotOffset) / kSlotSize
: 0;
static_assert(kSlotsPerSlab > 0, "Slab size must fit at least one object");
// Helper methods.
// Calculates the memory address for a given slot index in a given Slab.
static void* SlotAddress(Slab* slab, size_t index) {
auto* base = reinterpret_cast<std::byte*>(slab);
return static_cast<void*>(base + kFirstSlotOffset + (index * kSlotSize));
}
// Pushes a Slab on the front of a doubly-linked list.
static void PushFront(Slab*& head, Slab* slab) {
slab->prev = nullptr;
slab->next = head;
if (head != nullptr) {
head->prev = slab;
}
head = slab;
}
// Removes a Slab from a doubly-linked list.
static void RemoveFromList(Slab*& head, Slab* slab) {
if (slab->prev != nullptr) {
slab->prev->next = slab->next;
} else {
head = slab->next;
}
if (slab->next != nullptr) {
slab->next->prev = slab->prev;
}
slab->prev = nullptr;
slab->next = nullptr;
}
// Swaps a Slab in a doubly-linked list with the next element.
static void SwapWithNext(Slab*& head, Slab* a) {
Slab* b = a->next;
if (b == nullptr) {
return;
}
Slab* prev = a->prev;
Slab* next = b->next;
if (prev != nullptr) {
prev->next = b;
} else {
head = b;
}
b->prev = prev;
b->next = a;
a->prev = b;
a->next = next;
if (next != nullptr) {
next->prev = a;
}
}
// Returns the next available slot, allocating a new Slab if necessary.
void* Allocate() {
if (non_full_slabs_ == nullptr) {
Slab* slab = nullptr;
if (spare_empty_slab_ != nullptr) {
slab = spare_empty_slab_;
spare_empty_slab_ = nullptr;
} else {
slab = CreateSlab();
}
PushFront(non_full_slabs_, slab);
}
Slab* slab = non_full_slabs_;
void* slot = nullptr;
// 1. Reuse from intrusive freelist if available.
if (slab->free_head != nullptr) {
FreeNode* node = slab->free_head;
#ifdef ABSL_HAVE_ADDRESS_SANITIZER
ASAN_UNPOISON_MEMORY_REGION(node, kSlotSize);
#endif
slab->free_head = node->next;
slot = static_cast<void*>(node);
} else {
// 2. Bump-allocate from uninitialized slot range.
QUICHE_DCHECK(slab->allocated_high_watermark < kSlotsPerSlab);
slot = SlotAddress(slab, slab->allocated_high_watermark);
#ifdef ABSL_HAVE_ADDRESS_SANITIZER
ASAN_UNPOISON_MEMORY_REGION(slot, kSlotSize);
#endif
++slab->allocated_high_watermark;
}
++slab->live_objects;
--total_free_slots_;
// If slab reached capacity, move it to full_slabs_.
if (slab->live_objects == kSlotsPerSlab) {
RemoveFromList(non_full_slabs_, slab);
PushFront(full_slabs_, slab);
}
return slot;
}
// Deallocates a slot. May result in the owning Slab being freed.
void Deallocate(void* ptr) noexcept {
// The owning slab can be found with a simple bitmask operation.
Slab* slab = reinterpret_cast<Slab*>(reinterpret_cast<uintptr_t>(ptr) &
~(SlabSizeBytes - 1));
QUICHE_DCHECK(slab != nullptr);
auto* node = static_cast<FreeNode*>(ptr);
node->next = slab->free_head;
slab->free_head = node;
#ifdef ABSL_HAVE_ADDRESS_SANITIZER
ASAN_POISON_MEMORY_REGION(ptr, kSlotSize);
#endif
const bool was_full = (slab->live_objects == kSlotsPerSlab);
--slab->live_objects;
++total_free_slots_;
if (slab->live_objects == 0) {
// Slab has no more live objects.
if (was_full) {
RemoveFromList(full_slabs_, slab);
} else {
RemoveFromList(non_full_slabs_, slab);
}
if (spare_empty_slab_ == nullptr) {
spare_empty_slab_ = slab;
} else {
DestroySlab(slab);
}
return;
}
if (was_full) {
// Move from full_slabs_ to front of non_full_slabs_ (as the fullest
// partial slab).
RemoveFromList(full_slabs_, slab);
PushFront(non_full_slabs_, slab);
} else {
// Keep non_full_slabs_ ordered by descending live_objects.
while (slab->next != nullptr &&
slab->live_objects < slab->next->live_objects) {
SwapWithNext(non_full_slabs_, slab);
}
}
}
// Allocates memory for and initializes a Slab.
Slab* CreateSlab() {
void* memory =
::operator new(SlabSizeBytes, std::align_val_t(SlabSizeBytes));
Slab* slab = ::new (memory) Slab();
#ifdef ABSL_HAVE_ADDRESS_SANITIZER
ASAN_POISON_MEMORY_REGION(
reinterpret_cast<std::byte*>(memory) + kFirstSlotOffset,
SlabSizeBytes - kFirstSlotOffset);
#endif
++allocated_slabs_count_;
total_free_slots_ += kSlotsPerSlab;
return slab;
}
// Deallocates memory for a given Slab.
void DestroySlab(Slab* slab) noexcept {
--allocated_slabs_count_;
total_free_slots_ -= kSlotsPerSlab;
slab->~Slab();
::operator delete(slab, std::align_val_t(SlabSizeBytes));
}
Slab* non_full_slabs_ = nullptr;
Slab* full_slabs_ = nullptr;
Slab* spare_empty_slab_ = nullptr;
size_t allocated_slabs_count_ = 0;
size_t total_free_slots_ = 0;
};
} // namespace quiche
#endif // QUICHE_COMMON_ABIDING_OBJECT_POOL_H_