TLA Line data Source code
1 : //
2 : // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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 : BOOST_CAPY_MSVC_WARNING_PUSH
50 : 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 : {
53 : 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 max_class_size = 2048; // 2^11
56 : static constexpr std::size_t bucket_capacity = 16;
57 :
58 : static std::size_t
59 HIT 9930 : round_up_pow2(std::size_t n) noexcept
60 : {
61 9930 : return n <= min_class_size ? min_class_size : std::bit_ceil(n);
62 : }
63 :
64 : static std::size_t
65 9930 : get_class_index(std::size_t rounded) noexcept
66 : {
67 9930 : std::size_t idx = std::countr_zero(rounded) - 6; // 64 = 2^6
68 9930 : return idx < num_classes ? idx : num_classes;
69 : }
70 :
71 : struct bucket
72 : {
73 : std::size_t count = 0;
74 : void* ptrs[bucket_capacity];
75 :
76 5072 : void* pop() noexcept
77 : {
78 5072 : if(count == 0)
79 107 : return nullptr;
80 4965 : return ptrs[--count];
81 : }
82 :
83 : // Peter Dimov's idea
84 107 : void* pop(bucket& b) noexcept
85 : {
86 107 : if(count == 0)
87 107 : return nullptr;
88 MIS 0 : for(std::size_t i = 0; i < count; ++i)
89 0 : b.ptrs[i] = ptrs[i];
90 0 : b.count = count - 1;
91 0 : count = 0;
92 0 : return b.ptrs[b.count];
93 : }
94 :
95 HIT 4969 : bool push(void* p) noexcept
96 : {
97 4969 : if(count >= bucket_capacity)
98 4 : return false;
99 4965 : ptrs[count++] = p;
100 4965 : return true;
101 : }
102 : };
103 :
104 : struct pool
105 : {
106 : bucket buckets[num_classes];
107 :
108 67 : ~pool()
109 : {
110 469 : for(auto& b : buckets)
111 509 : while(b.count > 0)
112 107 : ::operator delete(b.pop());
113 67 : }
114 : };
115 :
116 10037 : static pool& local() noexcept
117 : {
118 10037 : static thread_local pool p;
119 10037 : return p;
120 : }
121 :
122 : static pool& global() noexcept;
123 : static std::mutex& global_mutex() noexcept;
124 :
125 : void* allocate_slow(std::size_t rounded, std::size_t idx);
126 : void deallocate_slow(void* p, std::size_t idx);
127 :
128 : public:
129 : ~recycling_memory_resource();
130 :
131 : /** Allocate without virtual dispatch.
132 :
133 : Handles the fast path inline (thread-local bucket pop)
134 : and falls through to the slow path for global pool or
135 : heap allocation.
136 : */
137 : void*
138 4965 : allocate_fast(std::size_t bytes, std::size_t)
139 : {
140 4965 : std::size_t rounded = round_up_pow2(bytes);
141 4965 : std::size_t idx = get_class_index(rounded);
142 4965 : if(idx >= num_classes)
143 MIS 0 : return ::operator new(bytes);
144 HIT 4965 : auto& lp = local();
145 4965 : if(auto* p = lp.buckets[idx].pop())
146 4858 : return p;
147 107 : return allocate_slow(rounded, idx);
148 : }
149 :
150 : /** Deallocate without virtual dispatch.
151 :
152 : Handles the fast path inline (thread-local bucket push)
153 : and falls through to the slow path for global pool or
154 : heap deallocation.
155 : */
156 : void
157 4965 : deallocate_fast(void* p, std::size_t bytes, std::size_t)
158 : {
159 4965 : std::size_t rounded = round_up_pow2(bytes);
160 4965 : std::size_t idx = get_class_index(rounded);
161 4965 : if(idx >= num_classes)
162 : {
163 MIS 0 : ::operator delete(p);
164 0 : return;
165 : }
166 HIT 4965 : auto& lp = local();
167 4965 : if(lp.buckets[idx].push(p))
168 4961 : return;
169 4 : deallocate_slow(p, idx);
170 : }
171 :
172 : protected:
173 : void*
174 : do_allocate(std::size_t bytes, std::size_t) override;
175 :
176 : void
177 : do_deallocate(void* p, std::size_t bytes, std::size_t) override;
178 :
179 : bool
180 MIS 0 : do_is_equal(const memory_resource& other) const noexcept override
181 : {
182 0 : return this == &other;
183 : }
184 : };
185 : BOOST_CAPY_MSVC_WARNING_POP
186 :
187 : /** Returns pointer to the default recycling memory resource.
188 :
189 : The returned pointer is valid for the lifetime of the program.
190 : This is the default allocator used by run_async.
191 :
192 : @return Pointer to the recycling memory resource.
193 :
194 : @see recycling_memory_resource
195 : @see run_async
196 : */
197 : BOOST_CAPY_DECL
198 : std::pmr::memory_resource*
199 : get_recycling_memory_resource() noexcept;
200 :
201 : } // namespace capy
202 : } // namespace boost
203 :
204 : #endif
|