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

81.4% Lines (48/59) 90.0% Functions (9/10) 73.9% Branches (17/23)
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 <cstddef>
16 #include <memory_resource>
17 #include <mutex>
18
19 namespace boost {
20 namespace capy {
21
22 /** Recycling memory resource with thread-local and global pools.
23
24 This memory resource recycles memory blocks to reduce allocation
25 overhead for coroutine frames. It maintains a thread-local pool
26 for fast lock-free access and a global pool for cross-thread
27 block sharing.
28
29 Blocks are tracked by size to avoid returning undersized blocks.
30
31 This is the default allocator used by run_async when no allocator
32 is specified.
33
34 @par Thread Safety
35 Thread-safe. The thread-local pool requires no synchronization.
36 The global pool uses a mutex for cross-thread access.
37
38 @par Example
39 @code
40 auto* mr = get_recycling_memory_resource();
41 run_async(ex, mr)(my_task());
42 @endcode
43
44 @see get_recycling_memory_resource
45 @see run_async
46 */
47 class recycling_memory_resource : public std::pmr::memory_resource
48 {
49 struct block
50 {
51 block* next;
52 std::size_t size;
53 };
54
55 struct global_pool
56 {
57 std::mutex mtx;
58 block* head = nullptr;
59
60 23 ~global_pool()
61 {
62
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 23 times.
23 while(head)
63 {
64 auto p = head;
65 head = head->next;
66 ::operator delete(p);
67 }
68 23 }
69
70 void push(block* b)
71 {
72 std::lock_guard<std::mutex> lock(mtx);
73 b->next = head;
74 head = b;
75 }
76
77 94 block* pop(std::size_t n)
78 {
79
1/1
✓ Branch 1 taken 94 times.
94 std::lock_guard<std::mutex> lock(mtx);
80 94 block** pp = &head;
81
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 94 times.
94 while(*pp)
82 {
83 if((*pp)->size >= n + sizeof(block))
84 {
85 block* p = *pp;
86 *pp = p->next;
87 return p;
88 }
89 pp = &(*pp)->next;
90 }
91 94 return nullptr;
92 94 }
93 };
94
95 struct local_pool
96 {
97 block* head = nullptr;
98
99 24 ~local_pool()
100 {
101
2/2
✓ Branch 0 taken 94 times.
✓ Branch 1 taken 24 times.
118 while(head)
102 {
103 94 auto p = head;
104 94 head = head->next;
105 94 ::operator delete(p);
106 }
107 24 }
108
109 4389 void push(block* b)
110 {
111 4389 b->next = head;
112 4389 head = b;
113 4389 }
114
115 4389 block* pop(std::size_t n)
116 {
117 4389 block** pp = &head;
118
2/2
✓ Branch 0 taken 5866 times.
✓ Branch 1 taken 94 times.
5960 while(*pp)
119 {
120
2/2
✓ Branch 0 taken 4295 times.
✓ Branch 1 taken 1571 times.
5866 if((*pp)->size >= n + sizeof(block))
121 {
122 4295 block* p = *pp;
123 4295 *pp = p->next;
124 4295 return p;
125 }
126 1571 pp = &(*pp)->next;
127 }
128 94 return nullptr;
129 }
130 };
131
132 8778 static local_pool& local()
133 {
134
2/2
✓ Branch 0 taken 24 times.
✓ Branch 1 taken 8754 times.
8778 static thread_local local_pool pool;
135 8778 return pool;
136 }
137
138 94 static global_pool& global()
139 {
140
3/4
✓ Branch 0 taken 23 times.
✓ Branch 1 taken 71 times.
✓ Branch 3 taken 23 times.
✗ Branch 4 not taken.
94 static global_pool pool;
141 94 return pool;
142 }
143
144 protected:
145 void*
146 4389 do_allocate(std::size_t bytes, std::size_t) override
147 {
148 4389 std::size_t total = bytes + sizeof(block);
149
150
2/2
✓ Branch 2 taken 4295 times.
✓ Branch 3 taken 94 times.
4389 if(auto* b = local().pop(bytes))
151 4295 return static_cast<char*>(static_cast<void*>(b)) + sizeof(block);
152
153
1/2
✗ Branch 2 not taken.
✓ Branch 3 taken 94 times.
94 if(auto* b = global().pop(bytes))
154 return static_cast<char*>(static_cast<void*>(b)) + sizeof(block);
155
156 94 auto* b = static_cast<block*>(::operator new(total));
157 94 b->next = nullptr;
158 94 b->size = total;
159 94 return static_cast<char*>(static_cast<void*>(b)) + sizeof(block);
160 }
161
162 void
163 4389 do_deallocate(void* p, std::size_t, std::size_t) override
164 {
165 4389 auto* b = static_cast<block*>(
166 static_cast<void*>(static_cast<char*>(p) - sizeof(block)));
167 4389 b->next = nullptr;
168 4389 local().push(b);
169 4389 }
170
171 bool
172 do_is_equal(const memory_resource& other) const noexcept override
173 {
174 return this == &other;
175 }
176 };
177
178 /** Returns pointer to the default recycling memory resource.
179
180 The returned pointer is valid for the lifetime of the program.
181 This is the default allocator used by run_async.
182
183 @return Pointer to the recycling memory resource.
184
185 @see recycling_memory_resource
186 @see run_async
187 */
188 BOOST_CAPY_DECL
189 std::pmr::memory_resource*
190 get_recycling_memory_resource() noexcept;
191
192 } // namespace capy
193 } // namespace boost
194
195 #endif
196