src/ex/recycling_memory_resource.cpp

100.0% Lines (53 / 53) 100.0% Functions (13 / 13)
recycling_memory_resource.cpp
f(x) Functions (13)
Line TLA Hits 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 #include <boost/capy/ex/recycling_memory_resource.hpp>
11
12 #include <new>
13 #include <tuple>
14
15 namespace boost {
16 namespace capy {
17
18 // Instance destruction does nothing: the resource is stateless (all pools
19 // are static). Global-pool cleanup is owned by global()'s holder, exactly
20 // as in the original where the global pool's own destructor drained it,
21 // independent of any instance lifetime.
22 307x recycling_memory_resource::~recycling_memory_resource() = default;
23
24 #if (defined(BOOST_CAPY_DYN_LINK) || defined(BOOST_ALL_DYN_LINK)) && \
25 !defined(BOOST_CAPY_STATIC_LINK)
26
27 recycling_memory_resource::pool&
28 644349x recycling_memory_resource::local() noexcept
29 {
30 static thread_local pool p;
31 644349x return p;
32 }
33
34 bool&
35 216943x recycling_memory_resource::thread_armed() noexcept
36 {
37 static thread_local bool armed = false;
38 216943x return armed;
39 }
40
41 #endif
42
43 namespace {
44
45 // Set once this thread's pool was drained at exit. Another thread_local
46 // destroyed later can still free frames; they must not be cached.
47 thread_local bool pool_drained = false;
48
49 } // namespace
50
51 bool
52 211195x recycling_memory_resource::arm_thread_cleanup() noexcept
53 {
54 struct janitor
55 {
56 365x ~janitor()
57 {
58 // Return this thread's cached blocks to the OS so nothing
59 // leaks at thread exit. The pool has a trivial dtor, so its
60 // thread-local storage is still valid here.
61 365x auto& lp = local();
62 2555x for(auto& b : lp.buckets)
63 6728x while(b.count > 0)
64 4538x ::operator delete(b.pop());
65 365x pool_drained = true;
66 365x thread_armed() = false;
67 365x }
68 };
69 211195x if(pool_drained)
70 5x return false;
71 211190x static thread_local janitor j;
72 211190x std::ignore = j;
73 211190x return true;
74 }
75
76 recycling_memory_resource::pool&
77 420498x recycling_memory_resource::global() noexcept
78 {
79 // Holder gives the global pool a destructor (the trivial-dtor pool type
80 // itself must stay guard-free for local()). Runs unconditionally at
81 // process exit, mirroring the original global pool destructor, and is
82 // locked because a worker thread may still be in a slow path. This path
83 // is cold (slow paths only), so the holder's guard costs nothing hot.
84 struct holder
85 {
86 pool p;
87
88 34x ~holder()
89 {
90 34x std::lock_guard<std::mutex> lock(global_mutex());
91 238x for(auto& b : p.buckets)
92 327x while(b.count > 0)
93 123x ::operator delete(b.pop());
94 34x }
95 };
96 420498x static holder h;
97 420498x return h.p;
98 }
99
100 std::mutex&
101 420532x recycling_memory_resource::global_mutex() noexcept
102 {
103 // Never destroyed: it is locked during process-exit teardown (in
104 // global()'s holder destructor), after a function-local `static
105 // std::mutex` could itself have been destroyed. Placement-new into
106 // static storage owns no heap allocation, so there is nothing to leak.
107 alignas(std::mutex) static unsigned char storage[sizeof(std::mutex)];
108 static std::mutex* const mtx =
109 420532x ::new(static_cast<void*>(storage)) std::mutex();
110 420532x return *mtx;
111 }
112
113 void*
114 210833x recycling_memory_resource::allocate_slow(
115 std::size_t rounded, std::size_t idx)
116 {
117 // Once this thread's pool was drained at exit, take one block and
118 // leave the pool empty: nothing would release a refill.
119 210833x bool const cache = arm_thread_cleanup();
120 {
121 210833x std::lock_guard<std::mutex> lock(global_mutex());
122 210833x auto& b = global().buckets[idx];
123 210833x if(auto* p = cache ? b.pop(local().buckets[idx]) : b.pop())
124 716x return p;
125 210833x }
126 210117x return ::operator new(rounded);
127 }
128
129 void
130 209665x recycling_memory_resource::deallocate_slow(
131 void* p, std::size_t idx)
132 {
133 {
134 209665x std::lock_guard<std::mutex> lock(global_mutex());
135 209665x if(global().buckets[idx].push(p))
136 4209x return;
137 209665x }
138 205456x ::operator delete(p);
139 }
140
141 void*
142 102470x recycling_memory_resource::do_allocate(std::size_t bytes, std::size_t alignment)
143 {
144 102470x return allocate_fast(bytes, alignment);
145 }
146
147 void
148 102470x recycling_memory_resource::do_deallocate(void* p, std::size_t bytes, std::size_t alignment)
149 {
150 102470x deallocate_fast(p, bytes, alignment);
151 102470x }
152
153 std::pmr::memory_resource*
154 4555x get_recycling_memory_resource() noexcept
155 {
156 4555x static recycling_memory_resource instance;
157 4555x return &instance;
158 }
159
160 } // namespace capy
161 } // namespace boost
162