93.75% Lines (45/48) 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   20  
21   namespace boost { 21   namespace boost {
22   namespace capy { 22   namespace capy {
23   23  
24   /** Recycles freed blocks through per-thread pools, with a shared pool for cross-thread reuse. 24   /** Recycles freed blocks through per-thread pools, with a shared pool for cross-thread reuse.
25   25  
26   This memory resource recycles memory blocks using power-of-two 26   This memory resource recycles memory blocks using power-of-two
27   size classes for O(1) allocation lookup. It maintains a thread-local 27   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 28   pool for fast lock-free access and a global pool for cross-thread
29   block sharing. 29   block sharing.
30   30  
31   Size classes: 64, 128, 256, 512, 1024, 2048 bytes. 31   Size classes: 64, 128, 256, 512, 1024, 2048 bytes.
32   Allocations larger than 2048 bytes bypass the pools entirely. 32   Allocations larger than 2048 bytes bypass the pools entirely.
33   33  
34   This is the default allocator used by run_async when no allocator 34   This is the default allocator used by run_async when no allocator
35   is specified. 35   is specified.
36   36  
37   @par Thread Safety 37   @par Thread Safety
38   Thread-safe. The thread-local pool requires no synchronization. 38   Thread-safe. The thread-local pool requires no synchronization.
39   The global pool uses a mutex for cross-thread access. 39   The global pool uses a mutex for cross-thread access.
40   40  
41   @par Example 41   @par Example
42 - @code 42 + @par !example example
43 - auto* mr = get_recycling_memory_resource(); 43 +
44 - run_async(ex, mr)(my_task());  
45 - @endcode  
46   44  
47   @see get_recycling_memory_resource 45   @see get_recycling_memory_resource
48   @see run_async 46   @see run_async
49   */ 47   */
50   BOOST_CAPY_MSVC_WARNING_PUSH 48   BOOST_CAPY_MSVC_WARNING_PUSH
51   BOOST_CAPY_MSVC_WARNING_DISABLE(4275) // non dll-interface base class 49   BOOST_CAPY_MSVC_WARNING_DISABLE(4275) // non dll-interface base class
52   class BOOST_CAPY_DECL recycling_memory_resource : public std::pmr::memory_resource 50   class BOOST_CAPY_DECL recycling_memory_resource : public std::pmr::memory_resource
53   { 51   {
54   static constexpr std::size_t num_classes = 6; 52   static constexpr std::size_t num_classes = 6;
55   static constexpr std::size_t min_class_size = 64; // 2^6 53   static constexpr std::size_t min_class_size = 64; // 2^6
56   static constexpr std::size_t max_class_size = 2048; // 2^11 54   static constexpr std::size_t max_class_size = 2048; // 2^11
57   static constexpr std::size_t bucket_capacity = 16; 55   static constexpr std::size_t bucket_capacity = 16;
58   56  
59   static std::size_t 57   static std::size_t
HITCBC 60   25532 round_up_pow2(std::size_t n) noexcept 58   25532 round_up_pow2(std::size_t n) noexcept
61   { 59   {
HITCBC 62   25532 return n <= min_class_size ? min_class_size : std::bit_ceil(n); 60   25532 return n <= min_class_size ? min_class_size : std::bit_ceil(n);
63   } 61   }
64   62  
65   static std::size_t 63   static std::size_t
HITCBC 66   25532 get_class_index(std::size_t rounded) noexcept 64   25532 get_class_index(std::size_t rounded) noexcept
67   { 65   {
HITCBC 68   25532 std::size_t idx = std::countr_zero(rounded) - 6; // 64 = 2^6 66   25532 std::size_t idx = std::countr_zero(rounded) - 6; // 64 = 2^6
HITCBC 69   25532 return idx < num_classes ? idx : num_classes; 67   25532 return idx < num_classes ? idx : num_classes;
70   } 68   }
71   69  
72   struct bucket 70   struct bucket
73   { 71   {
74   std::size_t count = 0; 72   std::size_t count = 0;
75   void* ptrs[bucket_capacity] = {}; 73   void* ptrs[bucket_capacity] = {};
76   74  
HITCBC 77   17059 void* pop() noexcept 75   17059 void* pop() noexcept
78   { 76   {
HITCBC 79   17059 if(count == 0) 77   17059 if(count == 0)
HITCBC 80   6897 return nullptr; 78   7025 return nullptr;
HITCBC 81   10162 return ptrs[--count]; 79   10034 return ptrs[--count];
82   } 80   }
83   81  
84   // Peter Dimov's idea 82   // Peter Dimov's idea
HITCBC 85   6897 void* pop(bucket& b) noexcept 83   7025 void* pop(bucket& b) noexcept
86   { 84   {
HITCBC 87   6897 if(count == 0) 85   7025 if(count == 0)
HITCBC 88   6306 return nullptr; 86   6281 return nullptr;
HITCBC 89   4820 for(std::size_t i = 0; i < count; ++i) 87   5088 for(std::size_t i = 0; i < count; ++i)
HITCBC 90   4229 b.ptrs[i] = ptrs[i]; 88   4344 b.ptrs[i] = ptrs[i];
HITCBC 91   591 b.count = count - 1; 89   744 b.count = count - 1;
HITCBC 92   591 count = 0; 90   744 count = 0;
HITCBC 93   591 return b.ptrs[b.count]; 91   744 return b.ptrs[b.count];
94   } 92   }
95   93  
HITCBC 96   19049 bool push(void* p) noexcept 94   19139 bool push(void* p) noexcept
97   { 95   {
HITCBC 98   19049 if(count >= bucket_capacity) 96   19139 if(count >= bucket_capacity)
HITCBC 99   8296 return false; 97   8361 return false;
HITCBC 100   10753 ptrs[count++] = p; 98   10778 ptrs[count++] = p;
HITCBC 101   10753 return true; 99   10778 return true;
102   } 100   }
103   }; 101   };
104   102  
105   struct pool 103   struct pool
106   { 104   {
107   bucket buckets[num_classes]; 105   bucket buckets[num_classes];
108   106  
109   // No destructor: a non-trivial dtor forces a guard variable on the 107   // No destructor: a non-trivial dtor forces a guard variable on the
110   // thread_local in local(), checked on every alloc/free. Constant 108   // thread_local in local(), checked on every alloc/free. Constant
111   // initialization plus a trivial dtor makes that access a bare TLS 109   // initialization plus a trivial dtor makes that access a bare TLS
112   // load. Cached blocks are instead reclaimed explicitly: per-thread 110   // load. Cached blocks are instead reclaimed explicitly: per-thread
113   // by arm_thread_cleanup() at thread exit, and the global pool by 111   // by arm_thread_cleanup() at thread exit, and the global pool by
114   // global()'s holder destructor at process exit. 112   // global()'s holder destructor at process exit.
115   }; 113   };
116   114  
HITCBC 117   32718 static pool& local() noexcept 115   32846 static pool& local() noexcept
118   { 116   {
119   static thread_local pool p; 117   static thread_local pool p;
HITCBC 120   32718 return p; 118   32846 return p;
121   } 119   }
122   120  
123   static pool& global() noexcept; 121   static pool& global() noexcept;
124   static std::mutex& global_mutex() noexcept; 122   static std::mutex& global_mutex() noexcept;
125   123  
126   void* allocate_slow(std::size_t rounded, std::size_t idx); 124   void* allocate_slow(std::size_t rounded, std::size_t idx);
127   void deallocate_slow(void* p, std::size_t idx); 125   void deallocate_slow(void* p, std::size_t idx);
128   126  
129   // Register a thread-exit callback that drains this thread's local 127   // Register a thread-exit callback that drains this thread's local
130   // pool back to the OS. Called only off the hot path: unconditionally 128   // pool back to the OS. Called only off the hot path: unconditionally
131   // from the slow paths, and once per thread from deallocate_fast 129   // from the slow paths, and once per thread from deallocate_fast
132   // behind a guard-free flag. 130   // behind a guard-free flag.
133   static void arm_thread_cleanup() noexcept; 131   static void arm_thread_cleanup() noexcept;
134   132  
135   public: 133   public:
136   /** Destroy the resource. 134   /** Destroy the resource.
137   135  
138   No cached block is released here. Every pool is static, so an 136   No cached block is released here. Every pool is static, so an
139   instance holds no state of its own. The thread-local pool is 137   instance holds no state of its own. The thread-local pool is
140   drained at thread exit, and the global pool at process exit. 138   drained at thread exit, and the global pool at process exit.
141   */ 139   */
142   ~recycling_memory_resource(); 140   ~recycling_memory_resource();
143   141  
144   /** Allocate without virtual dispatch. 142   /** Allocate without virtual dispatch.
145   143  
146   Handles the fast path inline (thread-local bucket pop) 144   Handles the fast path inline (thread-local bucket pop)
147   and falls through to the slow path for global pool or 145   and falls through to the slow path for global pool or
148   heap allocation. 146   heap allocation.
149   147  
150   A request larger than the largest size class (2048 bytes) 148   A request larger than the largest size class (2048 bytes)
151   bypasses the pools and goes straight to `::operator new`. 149   bypasses the pools and goes straight to `::operator new`.
152   150  
153   The second parameter is the requested alignment, and it is ignored. 151   The second parameter is the requested alignment, and it is ignored.
154   Every block comes from `::operator new`, so blocks carry the 152   Every block comes from `::operator new`, so blocks carry the
155   implementation's default new alignment and no more. 153   implementation's default new alignment and no more.
156   154  
157   @param bytes The number of bytes to allocate. 155   @param bytes The number of bytes to allocate.
158   156  
159   @return A pointer to a block of at least `bytes` bytes. A pooled 157   @return A pointer to a block of at least `bytes` bytes. A pooled
160   block is rounded up to its size class, so it may be larger than 158   block is rounded up to its size class, so it may be larger than
161   requested. 159   requested.
162   160  
163   @throws std::bad_alloc If the underlying `::operator new` fails. 161   @throws std::bad_alloc If the underlying `::operator new` fails.
164   */ 162   */
165   void* 163   void*
HITCBC 166   12766 allocate_fast(std::size_t bytes, std::size_t) 164   12766 allocate_fast(std::size_t bytes, std::size_t)
167   { 165   {
HITCBC 168   12766 std::size_t rounded = round_up_pow2(bytes); 166   12766 std::size_t rounded = round_up_pow2(bytes);
HITCBC 169   12766 std::size_t idx = get_class_index(rounded); 167   12766 std::size_t idx = get_class_index(rounded);
HITCBC 170   12766 if(idx >= num_classes) 168   12766 if(idx >= num_classes)
MISUBC 171   return ::operator new(bytes); 169   return ::operator new(bytes);
HITCBC 172   12766 auto& lp = local(); 170   12766 auto& lp = local();
HITCBC 173   12766 if(auto* p = lp.buckets[idx].pop()) 171   12766 if(auto* p = lp.buckets[idx].pop())
HITCBC 174   5869 return p; 172   5741 return p;
HITCBC 175   6897 return allocate_slow(rounded, idx); 173   7025 return allocate_slow(rounded, idx);
176   } 174   }
177   175  
178   /** Deallocate without virtual dispatch. 176   /** Deallocate without virtual dispatch.
179   177  
180   Handles the fast path inline (thread-local bucket push) 178   Handles the fast path inline (thread-local bucket push)
181   and falls through to the slow path for global pool or 179   and falls through to the slow path for global pool or
182   heap deallocation. 180   heap deallocation.
183   181  
184   The block is cached in the pool of the thread that frees it, not 182   The block is cached in the pool of the thread that frees it, not
185   the thread that allocated it. 183   the thread that allocated it.
186   184  
187   The third parameter is the alignment the block was allocated with, 185   The third parameter is the alignment the block was allocated with,
188   and it is ignored, as it is on allocation. 186   and it is ignored, as it is on allocation.
189   187  
190   @param p The block to return. It must have come from 188   @param p The block to return. It must have come from
191   @ref allocate_fast or @ref do_allocate on this resource. 189   @ref allocate_fast or @ref do_allocate on this resource.
192   190  
193   @param bytes The size the block was allocated with. The size class 191   @param bytes The size the block was allocated with. The size class
194   is recomputed from it, so passing a different value puts the block 192   is recomputed from it, so passing a different value puts the block
195   in the wrong bucket. 193   in the wrong bucket.
196   */ 194   */
197   void 195   void
HITCBC 198   12766 deallocate_fast(void* p, std::size_t bytes, std::size_t) 196   12766 deallocate_fast(void* p, std::size_t bytes, std::size_t)
199   { 197   {
HITCBC 200   12766 std::size_t rounded = round_up_pow2(bytes); 198   12766 std::size_t rounded = round_up_pow2(bytes);
HITCBC 201   12766 std::size_t idx = get_class_index(rounded); 199   12766 std::size_t idx = get_class_index(rounded);
HITCBC 202   12766 if(idx >= num_classes) 200   12766 if(idx >= num_classes)
203   { 201   {
MISUBC 204   ::operator delete(p); 202   ::operator delete(p);
MISUBC 205   return; 203   return;
206   } 204   }
207   // Guard-free flag (constinit bool, trivial dtor): arms thread-exit 205   // Guard-free flag (constinit bool, trivial dtor): arms thread-exit
208   // cleanup exactly once for any thread that caches via deallocate, 206   // cleanup exactly once for any thread that caches via deallocate,
209   // including consumer threads that never hit a slow path. 207   // including consumer threads that never hit a slow path.
210   static thread_local bool armed = false; 208   static thread_local bool armed = false;
HITCBC 211   12766 if(!armed) 209   12766 if(!armed)
212   { 210   {
HITCBC 213   283 armed = true; 211   283 armed = true;
HITCBC 214   283 arm_thread_cleanup(); 212   283 arm_thread_cleanup();
215   } 213   }
HITCBC 216   12766 auto& lp = local(); 214   12766 auto& lp = local();
HITCBC 217   12766 if(lp.buckets[idx].push(p)) 215   12766 if(lp.buckets[idx].push(p))
HITCBC 218   6483 return; 216   6393 return;
HITCBC 219   6283 deallocate_slow(p, idx); 217   6373 deallocate_slow(p, idx);
220   } 218   }
221   219  
222   protected: 220   protected:
223   /** Allocate through the `std::pmr::memory_resource` interface. 221   /** Allocate through the `std::pmr::memory_resource` interface.
224   222  
225   Forwards to @ref allocate_fast, so it has that function's contract. 223   Forwards to @ref allocate_fast, so it has that function's contract.
226   Call `allocate_fast` directly to skip the virtual dispatch. 224   Call `allocate_fast` directly to skip the virtual dispatch.
227   225  
228   @param bytes The number of bytes to allocate. 226   @param bytes The number of bytes to allocate.
229   227  
230   @param alignment The requested alignment. It is ignored. 228   @param alignment The requested alignment. It is ignored.
231   229  
232   @return A pointer to a block of at least `bytes` bytes. 230   @return A pointer to a block of at least `bytes` bytes.
233   231  
234   @throws std::bad_alloc If the underlying `::operator new` fails. 232   @throws std::bad_alloc If the underlying `::operator new` fails.
235   */ 233   */
236   void* 234   void*
237   do_allocate(std::size_t bytes, std::size_t alignment) override; 235   do_allocate(std::size_t bytes, std::size_t alignment) override;
238   236  
239   /** Deallocate through the `std::pmr::memory_resource` interface. 237   /** Deallocate through the `std::pmr::memory_resource` interface.
240   238  
241   Forwards to @ref deallocate_fast, so it has that function's 239   Forwards to @ref deallocate_fast, so it has that function's
242   contract. 240   contract.
243   241  
244   @param p The block to return, as obtained from this resource. 242   @param p The block to return, as obtained from this resource.
245   243  
246   @param bytes The size the block was allocated with. 244   @param bytes The size the block was allocated with.
247   245  
248   @param alignment The alignment the block was allocated with. It is 246   @param alignment The alignment the block was allocated with. It is
249   ignored. 247   ignored.
250   */ 248   */
251   void 249   void
252   do_deallocate(void* p, std::size_t bytes, std::size_t alignment) override; 250   do_deallocate(void* p, std::size_t bytes, std::size_t alignment) override;
253   251  
254   /** Compare this resource with another for equality. 252   /** Compare this resource with another for equality.
255   253  
256   Equality is object identity: two distinct 254   Equality is object identity: two distinct
257   `recycling_memory_resource` objects compare unequal, even though the 255   `recycling_memory_resource` objects compare unequal, even though the
258   pools they draw from are static and therefore shared. 256   pools they draw from are static and therefore shared.
259   257  
260   @param other The resource to compare against. 258   @param other The resource to compare against.
261   259  
262   @return `true` if `other` is the same object as `*this`; otherwise 260   @return `true` if `other` is the same object as `*this`; otherwise
263   `false`. 261   `false`.
264   */ 262   */
265   bool 263   bool
HITCBC 266   2 do_is_equal(const memory_resource& other) const noexcept override 264   2 do_is_equal(const memory_resource& other) const noexcept override
267   { 265   {
HITCBC 268   2 return this == &other; 266   2 return this == &other;
269   } 267   }
270   }; 268   };
271   BOOST_CAPY_MSVC_WARNING_POP 269   BOOST_CAPY_MSVC_WARNING_POP
272   270  
273   /** Returns pointer to the default recycling memory resource. 271   /** Returns pointer to the default recycling memory resource.
274   272  
275   The returned pointer is valid for the lifetime of the program. 273   The returned pointer is valid for the lifetime of the program.
276   This is the default allocator used by run_async. 274   This is the default allocator used by run_async.
277   275  
278   @return Pointer to the recycling memory resource. 276   @return Pointer to the recycling memory resource.
279   277  
280   @see recycling_memory_resource 278   @see recycling_memory_resource
281   @see run_async 279   @see run_async
282   */ 280   */
283   BOOST_CAPY_DECL 281   BOOST_CAPY_DECL
284   std::pmr::memory_resource* 282   std::pmr::memory_resource*
285   get_recycling_memory_resource() noexcept; 283   get_recycling_memory_resource() noexcept;
286   284  
287   } // namespace capy 285   } // namespace capy
288   } // namespace boost 286   } // namespace boost
289   287  
290   #endif 288   #endif