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 101 block* pop(std::size_t n)
78 {
79
1/1
✓ Branch 1 taken 101 times.
101 std::lock_guard<std::mutex> lock(mtx);
80 101 block** pp = &head;
81
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 101 times.
101 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 101 return nullptr;
92 101 }
93 };
94
95 struct local_pool
96 {
97 block* head = nullptr;
98
99 24 ~local_pool()
100 {
101
2/2
✓ Branch 0 taken 101 times.
✓ Branch 1 taken 24 times.
125 while(head)
102 {
103 101 auto p = head;
104 101 head = head->next;
105 101 ::operator delete(p);
106 }
107 24 }
108
109 4495 void push(block* b)
110 {
111 4495 b->next = head;
112 4495 head = b;
113 4495 }
114
115 4495 block* pop(std::size_t n)
116 {
117 4495 block** pp = &head;
118
2/2
✓ Branch 0 taken 5978 times.
✓ Branch 1 taken 101 times.
6079 while(*pp)
119 {
120
2/2
✓ Branch 0 taken 4394 times.
✓ Branch 1 taken 1584 times.
5978 if((*pp)->size >= n + sizeof(block))
121 {
122 4394 block* p = *pp;
123 4394 *pp = p->next;
124 4394 return p;
125 }
126 1584 pp = &(*pp)->next;
127 }
128 101 return nullptr;
129 }
130 };
131
132 8990 static local_pool& local()
133 {
134
2/2
✓ Branch 0 taken 24 times.
✓ Branch 1 taken 8966 times.
8990 static thread_local local_pool pool;
135 8990 return pool;
136 }
137
138 101 static global_pool& global()
139 {
140
3/4
✓ Branch 0 taken 23 times.
✓ Branch 1 taken 78 times.
✓ Branch 3 taken 23 times.
✗ Branch 4 not taken.
101 static global_pool pool;
141 101 return pool;
142 }
143
144 protected:
145 void*
146 4495 do_allocate(std::size_t bytes, std::size_t) override
147 {
148 4495 std::size_t total = bytes + sizeof(block);
149
150
2/2
✓ Branch 2 taken 4394 times.
✓ Branch 3 taken 101 times.
4495 if(auto* b = local().pop(bytes))
151 4394 return static_cast<char*>(static_cast<void*>(b)) + sizeof(block);
152
153
1/2
✗ Branch 2 not taken.
✓ Branch 3 taken 101 times.
101 if(auto* b = global().pop(bytes))
154 return static_cast<char*>(static_cast<void*>(b)) + sizeof(block);
155
156 101 auto* b = static_cast<block*>(::operator new(total));
157 101 b->next = nullptr;
158 101 b->size = total;
159 101 return static_cast<char*>(static_cast<void*>(b)) + sizeof(block);
160 }
161
162 void
163 4495 do_deallocate(void* p, std::size_t, std::size_t) override
164 {
165 4495 auto* b = static_cast<block*>(
166 static_cast<void*>(static_cast<char*>(p) - sizeof(block)));
167 4495 b->next = nullptr;
168 4495 local().push(b);
169 4495 }
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