94.92% Lines (56/59) 100.00% Functions (9/9)
TLA Baseline Branch
Line Hits Code Line Hits Code
1   // 1   //
2   // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) 2   // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
3   // Copyright (c) 2026 Michael Vandeberg 3   // Copyright (c) 2026 Michael Vandeberg
4   // 4   //
5   // Distributed under the Boost Software License, Version 1.0. (See accompanying 5   // Distributed under the Boost Software License, Version 1.0. (See accompanying
6   // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) 6   // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
7   // 7   //
8   // Official repository: https://github.com/cppalliance/capy 8   // Official repository: https://github.com/cppalliance/capy
9   // 9   //
10   10  
11   #ifndef BOOST_CAPY_RECYCLING_MEMORY_RESOURCE_HPP 11   #ifndef BOOST_CAPY_RECYCLING_MEMORY_RESOURCE_HPP
12   #define BOOST_CAPY_RECYCLING_MEMORY_RESOURCE_HPP 12   #define BOOST_CAPY_RECYCLING_MEMORY_RESOURCE_HPP
13   13  
14   #include <boost/capy/detail/config.hpp> 14   #include <boost/capy/detail/config.hpp>
15   15  
16   #include <bit> 16   #include <bit>
17   #include <cstddef> 17   #include <cstddef>
18   #include <memory_resource> 18   #include <memory_resource>
19   #include <mutex> 19   #include <mutex>
  20 + #include <new>
20   21  
21   namespace boost { 22   namespace boost {
22   namespace capy { 23   namespace capy {
23   24  
24   /** Recycles freed blocks through per-thread pools, with a shared pool for cross-thread reuse. 25   /** Recycles freed blocks through per-thread pools, with a shared pool for cross-thread reuse.
25   26  
26   This memory resource recycles memory blocks using power-of-two 27   This memory resource recycles memory blocks using power-of-two
27   size classes for O(1) allocation lookup. It maintains a thread-local 28   size classes for O(1) allocation lookup. It maintains a thread-local
28   pool for fast lock-free access and a global pool for cross-thread 29   pool for fast lock-free access and a global pool for cross-thread
29   block sharing. 30   block sharing.
30   31  
31   Size classes: 64, 128, 256, 512, 1024, 2048 bytes. 32   Size classes: 64, 128, 256, 512, 1024, 2048 bytes.
32   Allocations larger than 2048 bytes bypass the pools entirely. 33   Allocations larger than 2048 bytes bypass the pools entirely.
33   34  
  35 + Each pool caches at most 256 KiB of blocks per size class:
  36 + 4096 blocks of the smallest class, 128 of the largest. A freed
  37 + block that fits in neither the thread's pool nor the global pool
  38 + goes back to `::operator delete`.
  39 +
  40 + Blocks a thread caches but stops using drain back to the global
  41 + pool. The drain runs as the thread goes on freeing blocks or
  42 + refilling from the global pool. Other threads can then reuse what a
  43 + burst leaves behind. Blocks the global pool has no room for go back
  44 + to `::operator delete`.
  45 +
34   This is the default allocator used by run_async when no allocator 46   This is the default allocator used by run_async when no allocator
35   is specified. 47   is specified.
36   48  
37   @par Thread Safety 49   @par Thread Safety
38   Thread-safe. The thread-local pool requires no synchronization. 50   Thread-safe. The thread-local pool requires no synchronization.
39   The global pool uses a mutex for cross-thread access. 51   The global pool uses a mutex for cross-thread access.
40   52  
41   @par Example 53   @par Example
42   @par !example example 54   @par !example example
43   55  
44   56  
45   @see @ref get_recycling_memory_resource 57   @see @ref get_recycling_memory_resource
46   @see @ref run_async 58   @see @ref run_async
47   */ 59   */
48   BOOST_CAPY_MSVC_WARNING_PUSH 60   BOOST_CAPY_MSVC_WARNING_PUSH
49   BOOST_CAPY_MSVC_WARNING_DISABLE(4275) // non dll-interface base class 61   BOOST_CAPY_MSVC_WARNING_DISABLE(4275) // non dll-interface base class
50   class BOOST_CAPY_DECL recycling_memory_resource : public std::pmr::memory_resource 62   class BOOST_CAPY_DECL recycling_memory_resource : public std::pmr::memory_resource
51   { 63   {
52   static constexpr std::size_t num_classes = 6; 64   static constexpr std::size_t num_classes = 6;
53   static constexpr std::size_t min_class_size = 64; // 2^6 65   static constexpr std::size_t min_class_size = 64; // 2^6
54   static constexpr std::size_t max_class_size = 2048; // 2^11 66   static constexpr std::size_t max_class_size = 2048; // 2^11
55 - static constexpr std::size_t bucket_capacity = 16; 67 + static constexpr int min_class_shift = std::countr_zero(min_class_size);
  68 + static constexpr std::size_t max_cached_bytes = 256 * 1024;
  69 +
  70 + // Frees on one thread, plus blocks it refills from the global pool,
  71 + // between decay passes. Allocations served from the thread's pool
  72 + // aren't counted, which keeps the counter off that path.
  73 + static constexpr std::size_t decay_period = 4096;
56   74  
57   static std::size_t 75   static std::size_t
HITCBC 58   433122 round_up_pow2(std::size_t n) noexcept 76   2287936 round_up_pow2(std::size_t n) noexcept
59   { 77   {
HITCBC 60   433122 return n <= min_class_size ? min_class_size : std::bit_ceil(n); 78   2287936 return n <= min_class_size ? min_class_size : std::bit_ceil(n);
61   } 79   }
62   80  
63   static std::size_t 81   static std::size_t
HITCBC 64   433122 get_class_index(std::size_t rounded) noexcept 82   2287936 get_class_index(std::size_t rounded) noexcept
65   { 83   {
ECB 66 - 433122 std::size_t idx = std::countr_zero(rounded) - 6; // 64 = 2^6 84 + auto const idx = static_cast<std::size_t>(
HITGNC   85 + 2287936 std::countr_zero(rounded) - min_class_shift);
HITCBC 67   433122 return idx < num_classes ? idx : num_classes; 86   2287936 return idx < num_classes ? idx : num_classes;
68   } 87   }
69   88  
  89 + static constexpr std::size_t
HITGNC   90 + 1410470 bucket_capacity(std::size_t idx) noexcept
  91 + {
HITGNC   92 + 1410470 return max_cached_bytes >> (idx + min_class_shift);
  93 + }
  94 +
  95 + // Cached blocks are linked through their own storage, so a bucket
  96 + // holds any number of them without a side array.
  97 + struct node
  98 + {
  99 + node* next;
  100 + };
  101 +
70   struct bucket 102   struct bucket
71   { 103   {
  104 + node* head = nullptr;
72   std::size_t count = 0; 105   std::size_t count = 0;
73 - void* ptrs[bucket_capacity] = {}; 106 +
  107 + // Fewest blocks cached since the last decay pass: that many sat
  108 + // unused through the whole period.
  109 + std::size_t low = 0;
74   110  
HITCBC 75   221201 void* pop() noexcept 111   1199427 void* pop() noexcept
76   { 112   {
HITCBC 77 - 221201 if(count == 0) 113 + 1199427 node* n = head;
HITGNC   114 + 1199427 if(n == nullptr)
HITCBC 78   210523 return nullptr; 115   302445 return nullptr;
HITCBC 79 - 10678 return ptrs[--count]; 116 + 896982 head = n->next;
HITGNC   117 + 896982 --count;
HITGNC   118 + 896982 if(count < low)
HITGNC   119 + 53349 low = count;
HITGNC   120 + 896982 return n;
80   } 121   }
81   122  
82   // Peter Dimov's idea 123   // Peter Dimov's idea
  124 + // `b` must be empty: its chain is overwritten.
HITCBC 83   210522 void* pop(bucket& b) noexcept 125   302443 void* pop(bucket& b) noexcept
84   { 126   {
HITCBC 85 - 210522 if(count == 0) 127 + 302443 if(head == nullptr)
HITCBC 86   209932 return nullptr; 128   302414 return nullptr;
HITCBC 87 - 4782 for(std::size_t i = 0; i < count; ++i) 129 + 29 b.head = head;
HITCBC 88 - 4192 b.ptrs[i] = ptrs[i]; 130 + 29 b.count = count;
HITCBC 89 - 590 b.count = count - 1; 131 + 29 head = nullptr;
HITCBC 90   590 count = 0; 132   29 count = 0;
HITCBC 91 - 590 return b.ptrs[b.count]; 133 + 29 low = 0;
HITGNC   134 + 29 return b.pop();
92   } 135   }
93   136  
HITCBC 94 - 426166 bool push(void* p) noexcept 137 + 1410348 bool push(void* p, std::size_t capacity) noexcept
95   { 138   {
HITCBC 96 - 426166 if(count >= bucket_capacity) 139 + 1410348 if(count >= capacity)
HITCBC 97   414898 return false; 140   473705 return false;
HITCBC 98 - 11268 ptrs[count++] = p; 141 + 936643 head = ::new(p) node{head};
HITGNC   142 + 936643 ++count;
HITCBC 99   11268 return true; 143   936643 return true;
100   } 144   }
101   }; 145   };
102   146  
103   struct pool 147   struct pool
104   { 148   {
105   bucket buckets[num_classes]; 149   bucket buckets[num_classes];
  150 + std::size_t arrivals = 0;
106   151  
107   // No destructor: a non-trivial dtor forces a guard variable on the 152   // No destructor: a non-trivial dtor forces a guard variable on the
108   // thread_local in local(), checked on every alloc/free. Constant 153   // thread_local in local(), checked on every alloc/free. Constant
109   // initialization plus a trivial dtor makes that access a bare TLS 154   // initialization plus a trivial dtor makes that access a bare TLS
110   // load. Cached blocks are instead reclaimed explicitly: per-thread 155   // load. Cached blocks are instead reclaimed explicitly: per-thread
111   // by arm_thread_cleanup() at thread exit, and the global pool by 156   // by arm_thread_cleanup() at thread exit, and the global pool by
112   // global()'s holder destructor at process exit. 157   // global()'s holder destructor at process exit.
113   }; 158   };
114   159  
115   #if (defined(BOOST_CAPY_DYN_LINK) || defined(BOOST_ALL_DYN_LINK)) && \ 160   #if (defined(BOOST_CAPY_DYN_LINK) || defined(BOOST_ALL_DYN_LINK)) && \
116   !defined(BOOST_CAPY_STATIC_LINK) 161   !defined(BOOST_CAPY_STATIC_LINK)
117   // A shared build defines this thread's pool, and the flag below, in 162   // A shared build defines this thread's pool, and the flag below, in
118   // the library: inline, each module would cache in a pool of its own, 163   // the library: inline, each module would cache in a pool of its own,
119   // which the library's thread-exit cleanup never drains. 164   // which the library's thread-exit cleanup never drains.
120   static pool& local() noexcept; 165   static pool& local() noexcept;
121   #else 166   #else
122   static pool& local() noexcept 167   static pool& local() noexcept
123   { 168   {
124   static thread_local pool p; 169   static thread_local pool p;
125   return p; 170   return p;
126   } 171   }
127   #endif 172   #endif
128   173  
129   static pool& global() noexcept; 174   static pool& global() noexcept;
130   static std::mutex& global_mutex() noexcept; 175   static std::mutex& global_mutex() noexcept;
131   176  
132   void* allocate_slow(std::size_t rounded, std::size_t idx); 177   void* allocate_slow(std::size_t rounded, std::size_t idx);
133   void deallocate_slow(void* p, std::size_t idx); 178   void deallocate_slow(void* p, std::size_t idx);
134   179  
  180 + // Move half of each bucket's idle blocks to the global pool, or
  181 + // release them when it has no room. Halving, rather than emptying,
  182 + // leaves a burst that skips one period half its blocks.
  183 + static void decay(pool& lp) noexcept;
  184 +
135   // Register a thread-exit callback that drains this thread's local 185   // Register a thread-exit callback that drains this thread's local
136   // pool back to the OS. Called only off the hot path: unconditionally 186   // pool back to the OS. Called only off the hot path: unconditionally
137   // from the slow paths, and from deallocate_fast while the flag below 187   // from the slow paths, and from deallocate_fast while the flag below
138   // is clear. Returns false once that callback has run: nothing drains 188   // is clear. Returns false once that callback has run: nothing drains
139   // the local pool after it, so frees must bypass it. 189   // the local pool after it, so frees must bypass it.
140   static bool arm_thread_cleanup() noexcept; 190   static bool arm_thread_cleanup() noexcept;
141   191  
142   // Guard-free flag (constinit bool, trivial dtor): set once cleanup is 192   // Guard-free flag (constinit bool, trivial dtor): set once cleanup is
143   // armed, and cleared again when the cleanup has run. 193   // armed, and cleared again when the cleanup has run.
144   #if (defined(BOOST_CAPY_DYN_LINK) || defined(BOOST_ALL_DYN_LINK)) && \ 194   #if (defined(BOOST_CAPY_DYN_LINK) || defined(BOOST_ALL_DYN_LINK)) && \
145   !defined(BOOST_CAPY_STATIC_LINK) 195   !defined(BOOST_CAPY_STATIC_LINK)
146   static bool& thread_armed() noexcept; 196   static bool& thread_armed() noexcept;
147   #else 197   #else
148   static bool& thread_armed() noexcept 198   static bool& thread_armed() noexcept
149   { 199   {
150   static thread_local bool armed = false; 200   static thread_local bool armed = false;
151   return armed; 201   return armed;
152   } 202   }
153   #endif 203   #endif
154   204  
155   public: 205   public:
156   /** Destroy the resource. 206   /** Destroy the resource.
157   207  
158   No cached block is released here. Every pool is static, so an 208   No cached block is released here. Every pool is static, so an
159   instance holds no state of its own. The thread-local pool is 209   instance holds no state of its own. The thread-local pool is
160   drained at thread exit, and the global pool at process exit. 210   drained at thread exit, and the global pool at process exit.
161   */ 211   */
162   ~recycling_memory_resource(); 212   ~recycling_memory_resource();
163   213  
164   /** Allocate without virtual dispatch. 214   /** Allocate without virtual dispatch.
165   215  
166   Handles the fast path inline (thread-local bucket pop) 216   Handles the fast path inline (thread-local bucket pop)
167   and falls through to the slow path for global pool or 217   and falls through to the slow path for global pool or
168   heap allocation. 218   heap allocation.
169   219  
170   A request larger than the largest size class (2048 bytes) 220   A request larger than the largest size class (2048 bytes)
171   bypasses the pools and goes straight to `::operator new`. 221   bypasses the pools and goes straight to `::operator new`.
172   222  
173   The second parameter is the requested alignment, and it is ignored. 223   The second parameter is the requested alignment, and it is ignored.
174   Every block comes from `::operator new`, so blocks carry the 224   Every block comes from `::operator new`, so blocks carry the
175   implementation's default new alignment and no more. 225   implementation's default new alignment and no more.
176   226  
  227 + @par Complexity
  228 + Amortized constant. Now and then a call also hands this thread's
  229 + idle cached blocks to the global pool, or releases them when it
  230 + has no room.
  231 +
177   @param bytes The number of bytes to allocate. 232   @param bytes The number of bytes to allocate.
178   233  
179   @return A pointer to a block of at least `bytes` bytes. A pooled 234   @return A pointer to a block of at least `bytes` bytes. A pooled
180   block is rounded up to its size class, so it may be larger than 235   block is rounded up to its size class, so it may be larger than
181   requested. 236   requested.
182   237  
183   @throws std::bad_alloc If the underlying `::operator new` fails. 238   @throws std::bad_alloc If the underlying `::operator new` fails.
184   */ 239   */
185   void* 240   void*
HITCBC 186   216561 allocate_fast(std::size_t bytes, std::size_t) 241   1143968 allocate_fast(std::size_t bytes, std::size_t)
187   { 242   {
HITCBC 188   216561 std::size_t rounded = round_up_pow2(bytes); 243   1143968 std::size_t rounded = round_up_pow2(bytes);
HITCBC 189   216561 std::size_t idx = get_class_index(rounded); 244   1143968 std::size_t idx = get_class_index(rounded);
HITCBC 190   216561 if(idx >= num_classes) 245   1143968 if(idx >= num_classes)
MISUBC 191   ✗ return ::operator new(bytes); 246   ✗ return ::operator new(bytes);
HITCBC 192 - 216561 auto& lp = local(); 247 + 1143968 if(auto* p = local().buckets[idx].pop())
DCB 193 - 216561 if(auto* p = lp.buckets[idx].pop())  
HITCBC 194   6038 return p; 248   841524 return p;
HITCBC 195   210523 return allocate_slow(rounded, idx); 249   302444 return allocate_slow(rounded, idx);
196   } 250   }
197   251  
198   /** Deallocate without virtual dispatch. 252   /** Deallocate without virtual dispatch.
199   253  
200   Handles the fast path inline (thread-local bucket push) 254   Handles the fast path inline (thread-local bucket push)
201   and falls through to the slow path for global pool or 255   and falls through to the slow path for global pool or
202   heap deallocation. 256   heap deallocation.
203   257  
204   The block is cached in the pool of the thread that frees it, not 258   The block is cached in the pool of the thread that frees it, not
205   the thread that allocated it. 259   the thread that allocated it.
206   260  
207   The third parameter is the alignment the block was allocated with, 261   The third parameter is the alignment the block was allocated with,
208   and it is ignored, as it is on allocation. 262   and it is ignored, as it is on allocation.
209   263  
  264 + @par Complexity
  265 + Amortized constant. Now and then a call also hands this thread's
  266 + idle cached blocks to the global pool, or releases them when it
  267 + has no room.
  268 +
210   @param p The block to return. It must have come from 269   @param p The block to return. It must have come from
211   @ref allocate_fast or @ref do_allocate on this resource. 270   @ref allocate_fast or @ref do_allocate on this resource.
212   271  
213   @param bytes The size the block was allocated with. The size class 272   @param bytes The size the block was allocated with. The size class
214   is recomputed from it, so passing a different value puts the block 273   is recomputed from it, so passing a different value puts the block
215   in the wrong bucket. 274   in the wrong bucket.
216   */ 275   */
217   void 276   void
HITCBC 218   216561 deallocate_fast(void* p, std::size_t bytes, std::size_t) 277   1143968 deallocate_fast(void* p, std::size_t bytes, std::size_t)
219   { 278   {
HITCBC 220   216561 std::size_t rounded = round_up_pow2(bytes); 279   1143968 std::size_t rounded = round_up_pow2(bytes);
HITCBC 221   216561 std::size_t idx = get_class_index(rounded); 280   1143968 std::size_t idx = get_class_index(rounded);
HITCBC 222   216561 if(idx >= num_classes) 281   1143968 if(idx >= num_classes)
223   { 282   {
MISUBC 224   ✗ ::operator delete(p); 283   ✗ ::operator delete(p);
MISUBC 225   ✗ return; 284   ✗ return;
226   } 285   }
227   // Arms thread-exit cleanup for any thread that caches via 286   // Arms thread-exit cleanup for any thread that caches via
228   // deallocate, including consumer threads that never hit a slow 287   // deallocate, including consumer threads that never hit a slow
229   // path; after that cleanup, the block bypasses the local pool. 288   // path; after that cleanup, the block bypasses the local pool.
HITCBC 230   216561 auto& armed = thread_armed(); 289   1143968 auto& armed = thread_armed();
HITCBC 231   216561 if(!armed) 290   1143968 if(!armed)
232   { 291   {
HITCBC 233   355 if(!arm_thread_cleanup()) 292   375 if(!arm_thread_cleanup())
234   { 293   {
HITCBC 235   4 deallocate_slow(p, idx); 294   4 deallocate_slow(p, idx);
HITCBC 236   4 return; 295   4 return;
237   } 296   }
HITCBC 238   351 armed = true; 297   371 armed = true;
239   } 298   }
HITCBC 240   216557 auto& lp = local(); 299   1143964 auto& lp = local();
HITCBC 241 - 216557 if(lp.buckets[idx].push(p)) 300 + 1143964 if(++lp.arrivals == decay_period)
HITGNC   301 + 272 decay(lp);
HITGNC   302 + 1143964 if(lp.buckets[idx].push(p, bucket_capacity(idx)))
HITCBC 242   6952 return; 303   877584 return;
HITCBC 243   209605 deallocate_slow(p, idx); 304   266380 deallocate_slow(p, idx);
244   } 305   }
245   306  
246   protected: 307   protected:
247   /** Allocate through the `std::pmr::memory_resource` interface. 308   /** Allocate through the `std::pmr::memory_resource` interface.
248   309  
249   Forwards to @ref allocate_fast, so it has that function's contract. 310   Forwards to @ref allocate_fast, so it has that function's contract.
250   Call `allocate_fast` directly to skip the virtual dispatch. 311   Call `allocate_fast` directly to skip the virtual dispatch.
251   312  
252   @param bytes The number of bytes to allocate. 313   @param bytes The number of bytes to allocate.
253   314  
254   @param alignment The requested alignment. It is ignored. 315   @param alignment The requested alignment. It is ignored.
255   316  
256   @return A pointer to a block of at least `bytes` bytes. 317   @return A pointer to a block of at least `bytes` bytes.
257   318  
258   @throws std::bad_alloc If the underlying `::operator new` fails. 319   @throws std::bad_alloc If the underlying `::operator new` fails.
259   */ 320   */
260   void* 321   void*
261   do_allocate(std::size_t bytes, std::size_t alignment) override; 322   do_allocate(std::size_t bytes, std::size_t alignment) override;
262   323  
263   /** Deallocate through the `std::pmr::memory_resource` interface. 324   /** Deallocate through the `std::pmr::memory_resource` interface.
264   325  
265   Forwards to @ref deallocate_fast, so it has that function's 326   Forwards to @ref deallocate_fast, so it has that function's
266   contract. 327   contract.
267   328  
268   @param p The block to return, as obtained from this resource. 329   @param p The block to return, as obtained from this resource.
269   330  
270   @param bytes The size the block was allocated with. 331   @param bytes The size the block was allocated with.
271   332  
272   @param alignment The alignment the block was allocated with. It is 333   @param alignment The alignment the block was allocated with. It is
273   ignored. 334   ignored.
274   */ 335   */
275   void 336   void
276   do_deallocate(void* p, std::size_t bytes, std::size_t alignment) override; 337   do_deallocate(void* p, std::size_t bytes, std::size_t alignment) override;
277   338  
278   /** Compare this resource with another for equality. 339   /** Compare this resource with another for equality.
279   340  
280   Equality is object identity: two distinct 341   Equality is object identity: two distinct
281   `recycling_memory_resource` objects compare unequal, even though the 342   `recycling_memory_resource` objects compare unequal, even though the
282   pools they draw from are static and therefore shared. 343   pools they draw from are static and therefore shared.
283   344  
284   @param other The resource to compare against. 345   @param other The resource to compare against.
285   346  
286   @return `true` if `other` is the same object as `*this`; otherwise 347   @return `true` if `other` is the same object as `*this`; otherwise
287   `false`. 348   `false`.
288   */ 349   */
289   bool 350   bool
HITCBC 290   2 do_is_equal(const memory_resource& other) const noexcept override 351   2 do_is_equal(const memory_resource& other) const noexcept override
291   { 352   {
HITCBC 292   2 return this == &other; 353   2 return this == &other;
293   } 354   }
294   }; 355   };
295   BOOST_CAPY_MSVC_WARNING_POP 356   BOOST_CAPY_MSVC_WARNING_POP
296   357  
297   /** Returns pointer to the default recycling memory resource. 358   /** Returns pointer to the default recycling memory resource.
298   359  
299   The returned pointer is valid for the lifetime of the program. 360   The returned pointer is valid for the lifetime of the program.
300   This is the default allocator used by run_async. 361   This is the default allocator used by run_async.
301   362  
302   @return Pointer to the recycling memory resource. 363   @return Pointer to the recycling memory resource.
303   364  
304   @see @ref recycling_memory_resource 365   @see @ref recycling_memory_resource
305   @see @ref run_async 366   @see @ref run_async
306   */ 367   */
307   BOOST_CAPY_DECL 368   BOOST_CAPY_DECL
308   std::pmr::memory_resource* 369   std::pmr::memory_resource*
309   get_recycling_memory_resource() noexcept; 370   get_recycling_memory_resource() noexcept;
310   371  
311   } // namespace capy 372   } // namespace capy
312   } // namespace boost 373   } // namespace boost
313   374  
314   #endif 375   #endif