libs/capy/include/boost/capy/ex/recycling_memory_resource.hpp

85.5% Lines (53/62) 91.7% Functions (11/12) 80.6% Branches (29/36)
libs/capy/include/boost/capy/ex/recycling_memory_resource.hpp
Line Branch Hits Source Code
1 //
2 // Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com)
3 //
4 // 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 //
7 // Official repository: https://github.com/cppalliance/capy
8 //
9
10 #ifndef BOOST_CAPY_RECYCLING_MEMORY_RESOURCE_HPP
11 #define BOOST_CAPY_RECYCLING_MEMORY_RESOURCE_HPP
12
13 #include <boost/capy/detail/config.hpp>
14
15 #include <bit>
16 #include <cstddef>
17 #include <memory_resource>
18 #include <mutex>
19
20 namespace boost {
21 namespace capy {
22
23 /** Recycling memory resource with size-class buckets.
24
25 This memory resource recycles memory blocks using power-of-two
26 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 block sharing.
29
30 Size classes: 64, 128, 256, 512, 1024, 2048 bytes.
31 Allocations larger than 2048 bytes bypass the pools entirely.
32
33 This is the default allocator used by run_async when no allocator
34 is specified.
35
36 @par Thread Safety
37 Thread-safe. The thread-local pool requires no synchronization.
38 The global pool uses a mutex for cross-thread access.
39
40 @par Example
41 @code
42 auto* mr = get_recycling_memory_resource();
43 run_async(ex, mr)(my_task());
44 @endcode
45
46 @see get_recycling_memory_resource
47 @see run_async
48 */
49 class recycling_memory_resource : public std::pmr::memory_resource
50 {
51 static constexpr std::size_t num_classes = 6;
52 static constexpr std::size_t min_class_size = 64; // 2^6
53 static constexpr std::size_t max_class_size = 2048; // 2^11
54 static constexpr std::size_t bucket_capacity = 16;
55
56 static std::size_t
57 9506 round_up_pow2(std::size_t n) noexcept
58 {
59
1/2
✓ Branch 0 taken 9506 times.
✗ Branch 1 not taken.
9506 return n <= min_class_size ? min_class_size : std::bit_ceil(n);
60 }
61
62 static std::size_t
63 9506 get_class_index(std::size_t rounded) noexcept
64 {
65 9506 std::size_t idx = std::countr_zero(rounded) - 6; // 64 = 2^6
66
2/2
✓ Branch 0 taken 9346 times.
✓ Branch 1 taken 160 times.
9506 return idx < num_classes ? idx : num_classes;
67 }
68
69 struct pool
70 {
71 struct bucket
72 {
73 std::size_t count = 0;
74 void* ptrs[bucket_capacity];
75
76 4790 void* pop() noexcept
77 {
78
2/2
✓ Branch 0 taken 117 times.
✓ Branch 1 taken 4673 times.
4790 if(count == 0)
79 117 return nullptr;
80 4673 return ptrs[--count];
81 }
82
83 // Peter Dimov's idea
84 117 void* pop(bucket& b) noexcept
85 {
86
1/2
✓ Branch 0 taken 117 times.
✗ Branch 1 not taken.
117 if(count == 0)
87 117 return nullptr;
88 for(std::size_t i = 0; i < count; ++i)
89 b.ptrs[i] = ptrs[i];
90 b.count = count - 1;
91 count = 0;
92 return b.ptrs[b.count];
93 }
94
95 4677 bool push(void* p) noexcept
96 {
97
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 4673 times.
4677 if(count >= bucket_capacity)
98 4 return false;
99 4673 ptrs[count++] = p;
100 4673 return true;
101 }
102 };
103
104 bucket buckets[num_classes];
105
106 49 ~pool()
107 {
108
2/2
✓ Branch 0 taken 294 times.
✓ Branch 1 taken 49 times.
343 for(auto& b : buckets)
109
2/2
✓ Branch 0 taken 117 times.
✓ Branch 1 taken 294 times.
411 while(b.count > 0)
110 117 ::operator delete(b.pop());
111 49 }
112 };
113
114 static pool&
115 9463 local() noexcept
116 {
117
2/2
✓ Branch 0 taken 25 times.
✓ Branch 1 taken 9438 times.
9463 static thread_local pool p;
118 9463 return p;
119 }
120
121 static pool&
122 121 global() noexcept
123 {
124
3/4
✓ Branch 0 taken 24 times.
✓ Branch 1 taken 97 times.
✓ Branch 3 taken 24 times.
✗ Branch 4 not taken.
121 static pool p;
125 121 return p;
126 }
127
128 static std::mutex&
129 121 global_mutex() noexcept
130 {
131 static std::mutex mtx;
132 121 return mtx;
133 }
134
135 protected:
136 void*
137 4753 do_allocate(std::size_t bytes, std::size_t) override
138 {
139 4753 std::size_t rounded = round_up_pow2(bytes);
140 4753 std::size_t idx = get_class_index(rounded);
141
142
2/2
✓ Branch 0 taken 80 times.
✓ Branch 1 taken 4673 times.
4753 if(idx >= num_classes)
143 80 return ::operator new(bytes);
144
145
2/2
✓ Branch 2 taken 4556 times.
✓ Branch 3 taken 117 times.
4673 if(auto* p = local().buckets[idx].pop())
146 4556 return p;
147
148 {
149
1/1
✓ Branch 2 taken 117 times.
117 std::lock_guard<std::mutex> lock(global_mutex());
150
1/2
✗ Branch 3 not taken.
✓ Branch 4 taken 117 times.
117 if(auto* p = global().buckets[idx].pop(local().buckets[idx]))
151 return p;
152 117 }
153
154 117 return ::operator new(rounded);
155 }
156
157 void
158 4753 do_deallocate(void* p, std::size_t bytes, std::size_t) override
159 {
160 4753 std::size_t rounded = round_up_pow2(bytes);
161 4753 std::size_t idx = get_class_index(rounded);
162
163
2/2
✓ Branch 0 taken 80 times.
✓ Branch 1 taken 4673 times.
4753 if(idx >= num_classes)
164 {
165 80 ::operator delete(p);
166 80 return;
167 }
168
169
2/2
✓ Branch 2 taken 4669 times.
✓ Branch 3 taken 4 times.
4673 if(local().buckets[idx].push(p))
170 4669 return;
171
172 {
173
1/1
✓ Branch 2 taken 4 times.
4 std::lock_guard<std::mutex> lock(global_mutex());
174
1/2
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 if(global().buckets[idx].push(p))
175 4 return;
176 4 }
177
178 ::operator delete(p);
179 }
180
181 bool
182 do_is_equal(const memory_resource& other) const noexcept override
183 {
184 return this == &other;
185 }
186 };
187
188 /** Returns pointer to the default recycling memory resource.
189
190 The returned pointer is valid for the lifetime of the program.
191 This is the default allocator used by run_async.
192
193 @return Pointer to the recycling memory resource.
194
195 @see recycling_memory_resource
196 @see run_async
197 */
198 BOOST_CAPY_DECL
199 std::pmr::memory_resource*
200 get_recycling_memory_resource() noexcept;
201
202 } // namespace capy
203 } // namespace boost
204
205 #endif
206