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