pbr-cpp-memory-pool 1.1.2
Fixed-block-size O(1) memory pool — C++17 with an ANSI C public surface
Loading...
Searching...
No Matches
instrumented_pool.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 Daniel Polo
3
4#ifndef IT_D4NP_MEMORYPOOL_INSTRUMENTED_POOL_HPP_
5#define IT_D4NP_MEMORYPOOL_INSTRUMENTED_POOL_HPP_
6
31
32#include <atomic>
33#include <cstddef>
34#include <cstdint>
35#include <new>
36#include <optional>
37#include <ostream>
38#include <utility>
39#include <vector>
40
41namespace it::d4np::memorypool {
42
49struct PoolStats {
50 std::size_t allocations_;
51 std::size_t deallocations_;
53 std::size_t live_;
54 std::size_t peak_live_;
55};
56
58enum class PoolEvent : std::uint8_t {
59 exhausted,
60 grew,
62};
63
75 PoolObserver() = default;
76 PoolObserver(const PoolObserver&) = default;
77 PoolObserver(PoolObserver&&) = default;
78 PoolObserver& operator=(const PoolObserver&) = default;
79 PoolObserver& operator=(PoolObserver&&) = default;
80 virtual ~PoolObserver() = default;
81
88 virtual void on_pool_event(PoolEvent event, const PoolStats& stats) noexcept = 0;
89};
90
99public:
101 explicit InstrumentedPool(Pool&& pool) noexcept : pool_(std::move(pool)) {}
102
104 [[nodiscard]] static std::optional<InstrumentedPool> make(std::size_t block_size, std::size_t block_count) {
105 std::optional<Pool> pool = Pool::make(block_size, block_count);
106 if (!pool.has_value()) {
107 return std::nullopt;
108 }
109 return {InstrumentedPool{std::move(*pool)}};
110 }
111
113 // NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
114 [[nodiscard]] static std::optional<InstrumentedPool> make_dynamic(std::size_t block_size, std::size_t block_count,
115 std::size_t growth_factor) {
116 std::optional<Pool> pool = Pool::make_dynamic(block_size, block_count, growth_factor);
117 if (!pool.has_value()) {
118 return std::nullopt;
119 }
120 return {InstrumentedPool{std::move(*pool)}};
121 }
122
123 InstrumentedPool(const InstrumentedPool&) = delete;
124 InstrumentedPool& operator=(const InstrumentedPool&) = delete;
125
129 : pool_(std::move(other.pool_)), allocations_(other.allocations_.load(std::memory_order_relaxed)),
130 deallocations_(other.deallocations_.load(std::memory_order_relaxed)),
131 allocation_failures_(other.allocation_failures_.load(std::memory_order_relaxed)),
132 live_(other.live_.load(std::memory_order_relaxed)),
133 peak_live_(other.peak_live_.load(std::memory_order_relaxed)), observers_(std::move(other.observers_)),
134 last_growths_(other.last_growths_.load(std::memory_order_relaxed)) {}
135
140 if (this != &other) {
141 notify(PoolEvent::destroyed);
142 pool_ = std::move(other.pool_);
143 allocations_.store(other.allocations_.load(std::memory_order_relaxed), std::memory_order_relaxed);
144 deallocations_.store(other.deallocations_.load(std::memory_order_relaxed), std::memory_order_relaxed);
145 allocation_failures_.store(other.allocation_failures_.load(std::memory_order_relaxed),
146 std::memory_order_relaxed);
147 live_.store(other.live_.load(std::memory_order_relaxed), std::memory_order_relaxed);
148 peak_live_.store(other.peak_live_.load(std::memory_order_relaxed), std::memory_order_relaxed);
149 observers_ = std::move(other.observers_);
150 last_growths_.store(other.last_growths_.load(std::memory_order_relaxed), std::memory_order_relaxed);
151 }
152 return *this;
153 }
154
157 notify(PoolEvent::destroyed);
158 }
159
161 void add_observer(PoolObserver& observer) {
162 observers_.push_back(&observer);
163 }
164
166 [[nodiscard]] void* allocate() {
167 void* block = nullptr;
168 try {
169 block = pool_.allocate();
170 } catch (const std::bad_alloc&) {
171 allocation_failures_.fetch_add(1U, std::memory_order_relaxed);
172 notify(PoolEvent::exhausted);
173 throw;
174 }
175 record_allocation();
176 notify_if_grew();
177 return block;
178 }
179
181 [[nodiscard]] void* try_allocate() noexcept {
182 void* const block = pool_.try_allocate();
183 if (block == nullptr) {
184 allocation_failures_.fetch_add(1U, std::memory_order_relaxed);
185 notify(PoolEvent::exhausted);
186 return nullptr;
187 }
188 record_allocation();
189 notify_if_grew();
190 return block;
191 }
192
194 void deallocate(void* block) noexcept {
195 if (block != nullptr) {
196 deallocations_.fetch_add(1U, std::memory_order_relaxed);
197 // Clamp at zero: a foreign or already-freed pointer is a no-op in the
198 // core (ADR-0012), so an unconditional fetch_sub would underflow `live_`
199 // to SIZE_MAX on such a call (BUG-0002). Decrement only while positive.
200 std::size_t live = live_.load(std::memory_order_relaxed);
201 while (live > 0U && !live_.compare_exchange_weak(live, live - 1U, std::memory_order_relaxed)) {
202 // `live` is reloaded by compare_exchange_weak on failure; retry.
203 }
204 }
205 pool_.deallocate(block);
206 }
207
209 [[nodiscard]] PoolStats stats() const noexcept {
210 return PoolStats{allocations_.load(std::memory_order_relaxed), deallocations_.load(std::memory_order_relaxed),
211 allocation_failures_.load(std::memory_order_relaxed), live_.load(std::memory_order_relaxed),
212 peak_live_.load(std::memory_order_relaxed)};
213 }
214
216 void write_summary(std::ostream& os) const {
217 const PoolStats snapshot = stats();
218 os << "InstrumentedPool: allocations=" << snapshot.allocations_ << " deallocations=" << snapshot.deallocations_
219 << " failures=" << snapshot.allocation_failures_ << " live=" << snapshot.live_
220 << " peak_live=" << snapshot.peak_live_ << "\n";
221 }
222
224 [[nodiscard]] memory_pool_t* native_handle() noexcept {
225 return pool_.native_handle();
226 }
227
229 [[nodiscard]] std::size_t block_size() const noexcept {
230 return pool_.block_size();
231 }
232
234 [[nodiscard]] std::size_t metadata_bytes() const noexcept {
235 return pool_.metadata_bytes();
236 }
237
238private:
240 void record_allocation() noexcept {
241 allocations_.fetch_add(1U, std::memory_order_relaxed);
242 const std::size_t live = live_.fetch_add(1U, std::memory_order_relaxed) + 1U;
243 // Relaxed compare-exchange max — lift peak_live_ to `live` if it is behind.
244 std::size_t peak = peak_live_.load(std::memory_order_relaxed);
245 while (peak < live && !peak_live_.compare_exchange_weak(peak, live, std::memory_order_relaxed)) {
246 // peak is reloaded by compare_exchange_weak on failure; retry.
247 }
248 }
249
251 void notify(PoolEvent event) noexcept {
252 const PoolStats snapshot = stats();
253 for (PoolObserver* const observer : observers_) {
254 observer->on_pool_event(event, snapshot);
255 }
256 }
257
260 void notify_if_grew() noexcept {
261 const std::size_t growths = ::memory_pool_growths(pool_.native_handle());
262 // Advance the high-water growth count atomically; the thread that wins the
263 // compare-exchange notifies `grew` exactly once per observed growth, so
264 // concurrent callers neither race on the counter nor double-notify (BUG-0001).
265 std::size_t last = last_growths_.load(std::memory_order_relaxed);
266 while (growths > last) {
267 if (last_growths_.compare_exchange_weak(last, growths, std::memory_order_relaxed)) {
268 notify(PoolEvent::grew);
269 break;
270 }
271 // `last` is reloaded by compare_exchange_weak on failure; re-check.
272 }
273 }
274
275 Pool pool_;
276 std::atomic<std::size_t> allocations_{0U};
277 std::atomic<std::size_t> deallocations_{0U};
278 std::atomic<std::size_t> allocation_failures_{0U};
279 std::atomic<std::size_t> live_{0U};
280 std::atomic<std::size_t> peak_live_{0U};
281 std::vector<PoolObserver*> observers_;
282 // Atomic: read+advanced on the hot allocation path (`notify_if_grew`), which the
283 // class contract allows to run concurrently over a thread-safe pool (BUG-0001).
284 std::atomic<std::size_t> last_growths_{0U};
285};
286
287} // namespace it::d4np::memorypool
288
289#endif // IT_D4NP_MEMORYPOOL_INSTRUMENTED_POOL_HPP_
Move-only Decorator over Pool that instruments allocation activity.
static std::optional< InstrumentedPool > make_dynamic(std::size_t block_size, std::size_t block_count, std::size_t growth_factor)
Factory mirroring Pool::make_dynamic (ADR-0024) — std::nullopt on failure.
InstrumentedPool & operator=(InstrumentedPool &&other) noexcept
Move-assign; releases the current pool and re-seeds the counters + observers.
void * allocate()
Throwing allocation verb (ADR-0016 §2).
InstrumentedPool(InstrumentedPool &&other) noexcept
Move-construct; the atomic counters are loaded and re-seeded (ADR-0025 §2).
void deallocate(void *block) noexcept
Return a block; counts the deallocation for any non-null pointer.
~InstrumentedPool()
Notify observers of destruction (ADR-0026); a moved-from instance has no observers.
std::size_t metadata_bytes() const noexcept
void write_summary(std::ostream &os) const
Write a one-line human-readable summary of the counters to os.
void add_observer(PoolObserver &observer)
Register an observer of lifecycle events (ADR-0026).
static std::optional< InstrumentedPool > make(std::size_t block_size, std::size_t block_count)
Factory mirroring Pool::make — std::nullopt on construction failure.
std::size_t block_size() const noexcept
InstrumentedPool(Pool &&pool) noexcept
Adopt pool by move and start instrumenting it.
void * try_allocate() noexcept
Non-throwing allocation verb (ADR-0016 §2).
Owning, non-copyable, move-only wrapper around a memory_pool_t*.
void deallocate(void *block) noexcept
Return a previously allocated block to the pool in O(1).
void * allocate()
Allocate one block in O(1) — throwing verb (ADR-0016 §2).
std::size_t block_size() const noexcept
Report the configured per-block size in bytes (ADR-0018 §3).
memory_pool_t * native_handle() noexcept
void * try_allocate() noexcept
Allocate one block in O(1) — non-throwing verb (ADR-0016 §2).
static std::optional< Pool > make_dynamic(std::size_t block_size, std::size_t block_count, std::size_t growth_factor)
Factory function for a dynamic-growth pool (spec §2.2, ADR-0022 / ADR-0024): on exhaustion it acquire...
std::size_t metadata_bytes() const noexcept
Report the per-pool metadata overhead in bytes (spec §3.2 / ADR-0015).
static std::optional< Pool > make(std::size_t block_size, std::size_t block_count)
Factory function returning an engaged std::optional<Pool> on successful construction or std::nullopt ...
PoolEvent
Pool lifecycle events delivered to observers (ADR-0026).
@ exhausted
an allocation found the pool exhausted (returned NULL / threw)
@ grew
the (dynamic) pool acquired an overflow chunk
@ destroyed
the instrumented pool is being destroyed
struct memory_pool memory_pool_t
Opaque handle to a memory pool instance.
Definition memory_pool.h:82
size_t memory_pool_growths(const memory_pool_t *pool)
Report how many times pool has grown — i.e.
C++17 RAII wrapper around the C memory pool.
Observer of pool-lifecycle events (the GoF Observer — ADR-0026).
virtual void on_pool_event(PoolEvent event, const PoolStats &stats) noexcept=0
Called once per event, with a snapshot of the pool's counters.
Copyable snapshot of an InstrumentedPool's counters (ADR-0025 §2).
std::size_t peak_live_
high-water mark of live_
std::size_t allocations_
successful allocations
std::size_t deallocations_
deallocate calls with a non-null block
std::size_t live_
currently outstanding blocks
std::size_t allocation_failures_
allocate/try_allocate that found the pool exhausted