VLink  2.1.0
A high-performance communication middleware
memory_pool.h
浏览该文件的文档.
1 /*
2  * Copyright (C) 2026 by Thun Lu. All rights reserved.
3  * Author: Thun Lu <thun.lu@zohomail.cn>
4  * Repo: https://github.com/thun-res/vlink
5  * _ __ __ _ __
6  * | | / / / / (_) ____ / /__
7  * | | / / / / / / / __ \ / //_/
8  * | |/ / / /___ / / / / / / / ,<
9  * |___/ /_____/ /_/ /_/ /_/ /_/|_|
10  *
11  * Licensed under the Apache License, Version 2.0 (the "License");
12  * you may not use this file except in compliance with the License.
13  * You may obtain a copy of the License at
14  *
15  * http://www.apache.org/licenses/LICENSE-2.0
16  *
17  * Unless required by applicable law or agreed to in writing, software
18  * distributed under the License is distributed on an "AS IS" BASIS,
19  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20  * See the License for the specific language governing permissions and
21  * limitations under the License.
22  */
23 
24 /**
25  * @file memory_pool.h
26  * @brief Size-class tiered memory pool with per-tier free lists and runtime statistics.
27  *
28  * @details
29  * @c MemoryPool dispatches each allocation request to one of a fixed pyramid of size classes.
30  * Every tier owns a small fixed set of singly-linked free-list shards plus one shared vector of
31  * upstream chunks. A tier starts on its primary shard and enables sharded dispatch only after
32  * repeated real lock contention is observed. Empty local shards steal at most
33  * @c Config::batch_size nodes at a time. Chunk capacity starts small and doubles
34  * geometrically until it reaches the configured @c blocks_per_chunk; sharding does not multiply
35  * chunk quotas.
36  * Requests larger than the biggest tier (or with an alignment stricter than
37  * @c alignof(std::max_align_t)) bypass the pool and route directly to @c ::operator @c new /
38  * @c ::operator @c delete.
39  *
40  * @par Tier / bucket source
41  *
42  * | Source | When used |
43  * | ------------------------------------- | ------------------------------------------------------ |
44  * | Caller-supplied @c Config | @c MemoryPool(const @c Config&) with non-empty tiers |
45  * | @c get_default_config() (level 0..9) | @c global_instance(true) |
46  * | Built-in level-3 balanced pyramid | Malformed input fallback or @c global_instance(false) |
47  * | Built-in level @c N row | @c MemoryPool(int @c level, @c bool @c prealloc) ctor |
48  *
49  * @par Allocation flow
50  *
51  * @verbatim
52  * allocate(bytes, align)
53  * |
54  * v
55  * +-----------------+ align > kBlockAlignment? +---------------------+
56  * | guard | ----------------------------------> | oversized bypass |
57  * +-----------------+ | ::operator new |
58  * | no +---------------------+
59  * v
60  * +-----------------+ no fit ----> oversized bypass
61  * | find_tier(bs) |
62  * +--------+--------+
63  * | tier hit
64  * v
65  * +-----------------+ free list non-empty -> pop block
66  * | per-tier shard |
67  * +--------+--------+
68  * | free list empty
69  * v
70  * +-----------------+ ::operator new for next chunk
71  * | upstream alloc | carve into N blocks of tier size
72  * +-----------------+
73  * @endverbatim
74  *
75  * @par Cleanup primitives
76  *
77  * | Method | What it releases | Concurrent traffic? |
78  * | ------------------ | --------------------------------------------- | ------------------- |
79  * | @c clear / @c trim | Only fully-free chunks; live blocks preserved | Safe |
80  * | @c ~MemoryPool | Every chunk unconditionally | Caller must quiesce |
81  *
82  * @par Example
83  * @code
84  * auto& pool = vlink::MemoryPool::global_instance();
85  * void* p = pool.allocate(512);
86  * pool.deallocate(p, 512); // sizes MUST match
87  *
88  * pool.trim(); // periodic memory reclaim
89  * @endcode
90  *
91  * @note Public methods are @c noexcept and safe for concurrent use. Different size classes do
92  * not contend, while same-tier traffic is distributed after repeated contention is observed.
93  * @c deallocate requires the same @p bytes value passed to @c allocate. @c allocate
94  * returns @c nullptr on upstream OOM and never throws.
95  */
96 
97 #pragma once
98 
99 #include <cstddef>
100 #include <cstdint>
101 #include <memory>
102 #include <vector>
103 
104 #include "./macros.h"
105 
106 namespace vlink {
107 
108 /**
109  * @class MemoryPool
110  * @brief Per-tier free-list pool with runtime statistics and oversized passthrough.
111  *
112  * @details
113  * Thread-safe. Each tier owns a fixed set of short-held free-list spin locks and a blocking
114  * growth lock. The primary free list preserves the single-thread fast path; same-tier traffic
115  * switches to sharded dispatch after repeated contention. Bypass mode -- selected by an empty
116  * tier list -- routes every request through the global allocator without any pooling.
117  */
119  public:
120  /**
121  * @brief Default block alignment for every pooled allocation.
122  *
123  * @details
124  * Equal to @c alignof(std::max_align_t). Stricter alignments bypass the pool and request
125  * memory directly through @c ::operator @c new with @c std::align_val_t.
126  */
127  static constexpr size_t kBlockAlignment = alignof(std::max_align_t);
128 
129  /**
130  * @brief Descriptor for one size class.
131  */
132  struct Tier final {
133  size_t max_size{0}; ///< Inclusive upper bound of the tier in bytes.
134  size_t blocks_per_chunk{0}; ///< Maximum blocks carved from a single upstream chunk.
135  };
136 
137  /**
138  * @brief Constructor configuration for tiers, preallocation and cross-shard batch size.
139  *
140  * @details
141  * @c prealloc controls whether the constructor immediately fills every tier to its full
142  * @c blocks_per_chunk quota. Default @c false keeps the lazy growth path. Preallocation is
143  * best effort; any tier whose @c ::operator @c new fails stays in lazy state and the
144  * constructor continues. @c batch_size limits how many nodes an empty shard transfers
145  * from another shard while holding its short free-list lock; @c 0 falls back to the default 16.
146  */
147  struct Config final {
148  std::vector<Tier> tiers; ///< Tier descriptors; empty or all-sentinel selects bypass mode.
149  bool prealloc{false}; ///< When @c true, eagerly fill every managed tier to its quota.
150  size_t batch_size{16U}; ///< Maximum nodes transferred per cross-shard steal; @c 0 uses 16.
151  };
152 
153  /**
154  * @brief Per-tier runtime statistics snapshot.
155  *
156  * @details
157  * Counters use relaxed atomics; @c in_use_blocks and the lifetime upstream fields are best
158  * effort under concurrent traffic, not globally atomic.
159  */
160  struct TierStats final {
161  size_t max_size{0}; ///< Configured @c max_size for this tier.
162  size_t blocks_per_chunk{0}; ///< Configured @c blocks_per_chunk for this tier.
163  size_t block_size{0}; ///< Effective block size after alignment rounding.
164  uint64_t hit_count{0}; ///< Allocations dispatched to this tier (resettable).
165  uint64_t deallocate_count{0}; ///< Deallocations dispatched to this tier (resettable).
166  uint64_t in_use_blocks{0}; ///< Best-effort @c hit_count @c - @c deallocate_count.
167  uint64_t chunk_count{0}; ///< Currently owned chunks; @c clear decrements by released count.
168  uint64_t upstream_alloc_count{0}; ///< Lifetime number of chunks fully installed in this tier.
169  uint64_t upstream_alloc_bytes{0}; ///< Lifetime bytes of those installed chunks.
170  };
171 
172  /**
173  * @brief Statistics for allocations that bypass the tier free lists.
174  *
175  * @details
176  * Captures requests whose size exceeds the largest tier or whose alignment exceeds
177  * @c kBlockAlignment.
178  */
179  struct OversizedStats final {
180  uint64_t alloc_count{0}; ///< Oversized allocations forwarded to the system allocator.
181  uint64_t alloc_bytes{0}; ///< Total bytes of oversized allocations.
182  uint64_t dealloc_count{0}; ///< Oversized deallocations observed.
183  };
184 
185  /**
186  * @brief Constructs an empty pool in bypass mode.
187  *
188  * @details
189  * Equivalent to @c MemoryPool(Config{}): every request hits @c ::operator @c new / @c delete.
190  */
192 
193  /**
194  * @brief Constructs a tiered pool using the built-in pyramid for @p level.
195  *
196  * @details
197  * Out-of-range values are clamped to @c [0, @c 9] and a warning is logged. Level @c 0 yields
198  * bypass mode; the @p prealloc flag is ignored in that case. Level @c 9 fully saturates to
199  * roughly 656 MiB of resident memory.
200  *
201  * @param level Built-in level in @c [0, @c 9].
202  * @param prealloc When @c true, fills every tier to its quota on construction (best effort).
203  * Default: @c false.
204  */
205  explicit MemoryPool(int level, bool prealloc = false);
206 
207  /**
208  * @brief Constructs a tiered pool with an explicit configuration.
209  *
210  * @details
211  * Empty @c config.tiers selects bypass mode. Sentinel entries with @c blocks_per_chunk
212  * @c == @c 0 declare a size ceiling but are stripped at construction; an all-sentinel list is
213  * therefore equivalent to bypass mode. Malformed input (non-monotonic ordering, duplicate
214  * @c max_size, zero @c max_size or @c max_size below the minimum block size) logs an error
215  * and silently falls back to the level-3 default pyramid. @c std::bad_alloc may propagate
216  * from internal vector growth.
217  *
218  * @param config Tier descriptors, preallocation flag and cross-shard steal batch size.
219  */
220  explicit MemoryPool(const Config& config);
221 
222  /**
223  * @brief Releases every owned chunk unconditionally.
224  *
225  * @warning The caller must guarantee no outstanding pooled block is in use and no other
226  * thread is calling @c allocate, @c deallocate, @c clear or @c trim on this instance.
227  * Use @c clear for a non-destructive trim.
228  */
230 
231  /**
232  * @brief Allocates @p bytes of memory from the appropriate tier.
233  *
234  * @details
235  * Routes to the first tier whose @c max_size is @c >= @p bytes. Requests larger than the
236  * biggest tier or with @p alignment @c > @c kBlockAlignment bypass the pool. @p bytes @c ==
237  * @c 0 routes to the smallest tier and still returns a unique non-null pointer that must be
238  * passed back to @c deallocate with the same @c 0 size. Never throws.
239  *
240  * @param bytes Requested size.
241  * @param alignment Required alignment (power of two). Default: @c kBlockAlignment.
242  * @return Pointer to allocated memory, or @c nullptr on upstream OOM.
243  */
244  [[nodiscard]] void* allocate(size_t bytes, size_t alignment = kBlockAlignment) noexcept;
245 
246  /**
247  * @brief Returns a block previously allocated through @c allocate to the pool.
248  *
249  * @param p Pointer returned by @c allocate. @c nullptr is a no-op.
250  * @param bytes Original size passed to @c allocate; MUST match.
251  * @param alignment Original alignment passed to @c allocate.
252  */
253  void deallocate(void* p, size_t bytes, size_t alignment = kBlockAlignment) noexcept;
254 
255  /**
256  * @brief Returns the number of live managed tiers.
257  *
258  * @details
259  * @c 0 indicates bypass mode. Sentinel entries are stripped at construction so the count
260  * reflects only tiers backed by a live free list.
261  *
262  * @return Tier count after sentinel stripping.
263  */
264  [[nodiscard]] size_t get_tier_count() const noexcept;
265 
266  /**
267  * @brief Returns a snapshot of per-tier statistics.
268  *
269  * @return Vector with one entry per live tier.
270  */
271  [[nodiscard]] std::vector<TierStats> get_stats() const noexcept;
272 
273  /**
274  * @brief Returns a snapshot of the oversized-passthrough statistics.
275  *
276  * @details
277  * Counters are loaded with relaxed ordering and are not a globally atomic snapshot.
278  *
279  * @return Aggregated counters for the oversized path.
280  */
281  [[nodiscard]] OversizedStats get_oversized_stats() const noexcept;
282 
283  /**
284  * @brief Resets per-call statistics counters to zero.
285  *
286  * @details
287  * Clears @c hit_count and @c deallocate_count on every tier and the @c oversized_* counters.
288  * Physical / lifetime state (@c chunk_count, @c upstream_alloc_count, @c upstream_alloc_bytes)
289  * is preserved.
290  */
291  void reset_stats() noexcept;
292 
293  /**
294  * @brief Releases only fully-free chunks; preserves chunks still backing live allocations.
295  *
296  * @details
297  * For each tier the free list is grouped by owning chunk; chunks whose free-node count equals
298  * their block capacity are released, others stay intact. @c chunk_count is decremented by
299  * the number of released chunks. Safe to call concurrently with @c allocate and
300  * @c deallocate. Per-tier work is @c O(C @c log @c C @c + @c F @c log @c C) while growth and
301  * all free-list shards are paused.
302  */
303  void clear() noexcept;
304 
305  /**
306  * @brief Alias of @c clear with a name suited to periodic-trim phrasing.
307  *
308  * @details
309  * Behaviour, complexity and thread-safety are identical to @c clear.
310  */
311  void trim() noexcept;
312 
313  /**
314  * @brief Returns the default configuration using the @c VLINK_MEMORY_* environment variables.
315  *
316  * @details
317  * Each level @c 0..9 maps to a hand-coded row of @c {max_size, @c blocks_per_chunk} pairs.
318  * Level @c 0 is bypass mode. Levels @c 1..9 return 19 entries covering 32 B to 16 MiB; the
319  * 1 MiB / 4 MiB / 8 MiB / 16 MiB ceilings activate at level @c 2 / @c 4 / @c 5 / @c 6 and are
320  * sentinels below those thresholds. Within each level the 32 B head tier carries twice the
321  * @c blocks_per_chunk of the 64 B tier to absorb high-density tiny allocations.
322  *
323  * @c VLINK_MEMORY_PREALLOC controls the @c prealloc flag; only the literal value @c "1"
324  * enables preallocation. @c VLINK_MEMORY_BATCH_SIZE overrides @c batch_size;
325  * it must be a positive integer and defaults to @c 16. Environment values are captured on
326  * the first call to @c get_default_config().
327  *
328  * @return @c Config ready to pass to the constructor.
329  */
330  [[nodiscard]] static Config get_default_config();
331 
332  /**
333  * @brief Returns the process-wide shared @c MemoryPool instance.
334  *
335  * @details
336  * Lazy Meyers singleton. Only the first call decides the configuration; subsequent calls
337  * return the same instance and ignore the argument.
338  *
339  * @warning Whichever value @p use_env_level takes on the first call is baked in for the
340  * rest of the program's lifetime.
341  *
342  * @param use_env_level @c true (default): use @c get_default_config and honour the
343  * environment. @c false: use the built-in level-3 pyramid.
344  * @return Reference to the global pool.
345  */
346  static MemoryPool& global_instance(bool use_env_level = true);
347 
348  private:
349  size_t find_tier(size_t bytes) const noexcept;
350 
351  struct Impl;
352  std::unique_ptr<Impl> impl_;
353 
355 };
356 
357 } // namespace vlink
Cross-platform macros for visibility, branch hints, copy prevention, singletons and string helpers.
#define VLINK_EXPORT
Definition: macros.h:81
#define VLINK_DISALLOW_COPY_AND_ASSIGN(classname)
Deletes the copy constructor and copy-assignment operator of classname.
Definition: macros.h:174