Flow 2.0.0
Flow project: Full implementation reference.
thread_lcl_ptr_impl.hpp
Go to the documentation of this file.
1/* Flow
2 * Copyright 2023 Akamai Technologies, Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the
5 * "License"); you may not use this file except in
6 * compliance with the License. You may obtain a copy
7 * of the License at
8 *
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in
12 * writing, software distributed under the License is
13 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
14 * CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing
16 * permissions and limitations under the License. */
17
18/// @file
19#pragma once
20
22#include <boost/core/noncopyable.hpp>
23#include <boost/unordered/unordered_flat_map.hpp>
24#include <cassert>
25#include <cstddef>
26#include <optional>
27
28namespace flow::util
29{
30// Types.
31
32/**
33 * Internal-use helper for Thread_local_ptr which encapsulates that guy's `thread_local`ly-accessed TLS payloads.
34 * It is unlikely it is useful, as of this writing, in other contexts. Its shape is fairly generic, actually,
35 * except the reset_release() + reset_set() semantics are more specialized to an implementing Thread_local_ptr.
36 *
37 * Please see the impl section of Thread_local_ptr doc header. It is required background.
38 *
39 * Do note that a given `Thread_local_ptr_cache`'s thread-local storage applies to all `Thread_local_ptr<T>`s
40 * in the thread, regardless of `T`. That is the (at least partial) reason it is separate from Thread_local_ptr
41 * as opposed to just inlined therein. (It is also arguably cleaner.)
42 *
43 * Impl notes
44 * ----------
45 * As noted above, the impl background is in Thread_local_ptr doc header; what we do, algorithmically speaking,
46 * is described there and -- perhaps aside from `reset()` division of labor into our two `reset_*()` -- is
47 * in and of itself simple. The way Thread_local_ptr combines a `*this` with `thread_specific_ptr` is not trivial;
48 * but again that is none of our business here in `*this`.
49 *
50 * However, due to the extremely short operations involved, there are some very-tactical perf-relevant items
51 * to explain.
52 *
53 * ### Choice of map ###
54 * We actually have not benchmarked various possibilities here; but generally `unordered_flat_map` has a great
55 * reputation as a default map type of choice. So that is what we use. As noted elsewhere an auto-incrementing
56 * 64-bit integer, used as key, has good hashing properties.
57 *
58 * ### Inlining our `get()` and `thread_local`s ###
59 * The Flow coding guide discourages explicit inlining, partially for style reasons and partially based on the
60 * idea that compiler's max-optimization mode (`-O3` for gcc, say) will auto-inline things quite well; with LTO
61 * picking up the slack across translation units: in this case specifically a translation unit (call it U.cpp)
62 * that `include`s this header thread_lcl_ptr_impl.hpp (by way of header-only thread_lcl_ptr.hpp) versus our
63 * thread_lcl_ptr_impl.cpp.
64 *
65 * That said, our task here is specifically perf-oriented -- we would not exist otherwise; the conceit is
66 * we fully mirror Boost's contract but do it faster for `get()` -- *and* the op involved is extremely quick
67 * to begin with in Boost's impl (e.g., 10 nanosec per `get()` on a quick server machine in 2026). The Flow
68 * coding guide specifically cites such a scenario as the (an) exception to the avoid-explicit-inlining rule.
69 *
70 * In this particular case, in fact, we found through benchmarking that it really matters. The fast-path
71 * operation we're optimizing is get() (which essentially provides Thread_local_ptr::get(), which is what we timed).
72 * Without `inline thread_local`s and `inline get()` -- placing both definitions in our .cpp instead -- we
73 * observed (using flow::perf for timing-with-repetition):
74 * - TL-map contains 1 `Thread_local_ptr_cache`s: `thread_specific_ptr` => 7 ns; `Thread_local_ptr` => 11 ns.
75 * - TL-map contains 16 `Thread_local_ptr_cache`s: `thread_specific_ptr` => 11 ns; `Thread_local_ptr` => 11 ns.
76 * - (Increasing object count => Boost's latency grows; ours stays ~flat.)
77 *
78 * So we do indeed do better... but only once there are a few `Thread_local_ptr<T>`s around for the thread.
79 * Not bad but not great; given all the complexity -- and the fact that our `reset()` (while not, usually, along the
80 * fast-path) is *slower* than Boost's -- we should really just always be better. After all, why not -- all we
81 * do is access 2 `thread_local`s and perform an allegedly-fast `unordered_flat_map` lookup: we *should* be fast.
82 *
83 * Turns out, the cross-translation-unit call latency -- in this environment at least -- accounts for over 50%
84 * of the overall `get()` latency. So, with `inline thread_local` x 2 and `inline get()`, we get the promised
85 * and desired result wherein we are just faster:
86 * - TL-map contains 1 `Thread_local_ptr_cache`s: `thread_specific_ptr` => 7 ns; `Thread_local_ptr` => 5 ns.
87 * - TL-map contains 16 `Thread_local_ptr_cache`s: `thread_specific_ptr` => 11 ns; `Thread_local_ptr` => 5 ns.
88 * - (Increasing object count => Boost's latency grows; ours stays ~flat.)
89 *
90 * So that is why we explicitly `inline` the `thread_local`s and get(). (The above results are with Linux/gcc `-O3`.)
91 *
92 * ### What about release() and `reset_*()`? ###
93 * Though they are assumed to not be fast-path, we `inline` them too -- why not? They *could* be called frequently
94 * in some scenarios, and there's no reason to treat them differently. (Ctor/dtor as of this writing are left alone.)
95 *
96 * In the above environment Thread_local_ptr::reset() and `thread_specific_ptr::reset()` result were as follows
97 * FYI. This is with the `inline`s as currently implemented.
98 * - `thread_specific_ptr` => 23 ns.
99 * - `Thread_local_ptr` => 41 ns.
100 *
101 * We are within an order of magnitude -- roughly 2x -- of Boost's thing; this makes sense, as we *do* invoke
102 * Boost's thing and then *also* do our own, evidently (and sense-makingly) somewhat-cheaper map-insert. Again
103 * though, the presumption is usually `reset()` is infrequent (in the ubquituous lazy-init pattern: 1x per thread
104 * per `*this`), while `get()` is potentially very frequent.
105 */
107 private boost::noncopyable
108{
109public:
110 // Types.
111
112 /// Encodes result of reset_release(), indicating what the calling Thread_local_ptr::reset() should do next.
113 enum class Reset_result
114 {
115 /**
116 * `*this` is operative; key was looked up (X = result, possibly null); X does not equal `new_value`;
117 * X, if not null, was deleted from `*this`; proceed with next phase(s) of `reset()`.
118 *
119 * `*this` shall remain operative for the time being; that is within the highest-level user function executing
120 * in this thread.
121 */
122 S_OK,
123
124 /**
125 * `*this` is operative; key was looked up (X = result, possibly null); X equals `new_value`;
126 * `reset()` should no-op.
127 *
128 * `*this` shall remain operative for the time being; that is within the highest-level user function executing
129 * in this thread.
130 */
131 S_DUPE,
132
133 /**
134 * `*this` is inoperative permanently (we are near thread exit or `exit()`); do not touch `*this` further;
135 * `reset()` should operate independently of `*this`.
136 *
137 * `*this` shall remain inoperative permanently in this thread.
138 */
139 S_INOPERATIVE
140 };
141
142 // Constructors/destructor.
143
144 /// Ctor. No apparent effect.
146
147 /**
148 * Dtor. Frees relevant TLS in *this* thread; but note that is all internal book-keeping. No user-specified
149 * cleanup occurs! As for the internal book-keeping: again, the cleanup (basically our part -- if any -- of the
150 * TL-map) is for the current thread; it is opportunistic really, just 'cuz. In another thread, if
151 * get() is not null at this time, then that part of the map will leak until thread exit.
152 */
154
155 // Methods.
156
157 /**
158 * Looks up and returns the value last placed via reset_release() + reset_set(), or null if those have not
159 * been invoked, for this thread; returns empty `optional` in the near-thread-exit-or-`exit()` eventuality
160 * wherein `*this` is permanently (for the remainder of this thread) inoperable.
161 *
162 * @return Empty if inoperable (near exit); else: what Thread_local_ptr::get() should return (possibly null).
163 */
164 inline std::optional<void*> get() const;
165
166 /**
167 * Phase 1.1 of Thread_local_ptr::reset(): Executes `reset(new_value)` up-to when one potentially calls
168 * `cleanup_func(old_value)`; so not including inserting `new_value`. For perf purposes it also returns
169 * certain information -- see Reset_result docs -- that informs the situation it has detected.
170 *
171 * @param new_value
172 * See Thread_local_ptr::reset().
173 * @return See above; especially see doc header(s) of Reset_result.
174 */
175 inline Reset_result reset_release(void* new_value);
176
177 /**
178 * Phase 3.2 of Thread_local_ptr::reset(): Executes `reset(new_value)` just-after the update of the canonical
179 * `thread_specific_ptr`; so after `reset_release(new_value)`. You must only call this after
180 * reset_release() and only if that returned Reset_result::S_OK. (So in particular get() shall not presently
181 * return empty object.)
182 *
183 * `new_value` must not be null. Rationale: there is just no point; reset_release() would have done any possible
184 * required work.
185 *
186 * @param new_value
187 * See Thread_local_ptr::reset(); however `new_value` must not be null; else assertion may trip.
188 */
189 inline void reset_set(void* new_value);
190
191 /**
192 * Our part of Thread_local_ptr::release(). Lacks return value though (unnecessary;
193 * `thread_specific_ptr::release()` is required anyway and will return the right thing).
194 */
195 inline void release();
196
197private:
198 // Types.
199
200 /**
201 * 64-bit (at worst) key for #Tlp_states_map, uniquely generated by Unique_id_holder.
202 * `boost::thread_specific_ptr` in this situation uses `this` pointers.
203 *
204 * ### Rationale ###
205 * Firstly it has better hashing behavior, we are told, compared to using a pointer as a hash-key.
206 *
207 * The second reason is arguably imprecise, in that due to having to cooperate with `boost::thread_specific_ptr`
208 * means we can't eliminate the problem entirely; but it is still a great property to have architecturally.
209 * It's this: the 64-bit ID is, across this process for *all time*, effectively unique. Therefore a `*this`
210 * will not have the same ID as another `*this`, no matter how long ago that one was relevant.
211 * Hand-wavily this means that even a leaked Per_tlp_state -- with no Thread_local_ptr_cache corresponding
212 * to it extant any longer -- will not cause trouble for some new Per_tlp_state; that would always have
213 * a different #per_tlp_key_t.
214 *
215 * @see Thread_local_ptr doc header, section "A corner case." It is relevant to the second property above.
216 * Relatedly note dtor ~Thread_local_ptr_cache().
217 */
219
220 /**
221 * The data stored in the thread-local class-wide Tlp_states_map, per originating Thread_local_ptr_cache.
222 * As of this writing it has only one member, but experience shows in these situations it's best to have
223 * a `struct` type read to go. Perf-wise it is identical.
224 */
226 {
227 // Data.
228
229 /// The argument `new_value` to reset_set(); never null. `get() == nullptr` means there is no Per_tlp_state.
231 }; // struct Per_tlp_state
232
233 /**
234 * The map for a given thread, with a Per_tlp_state per Thread_local_ptr_cache (<=> Thread_local_ptr<...>)
235 * such that the last `reset_set(X)` (<=> Thread_local_ptr::reset()) had `X != nullptr`.
236 */
237 using Tlp_states_map = boost::unordered_flat_map<per_tlp_key_t, Per_tlp_state>;
238
239 /// Encapsulates two things: the thread's #Tlp_states_map; and what shall run at `thread_local` deinit.
241 {
242 // Constructors/destructor.
243
244 /**
245 * Ctor: registers (`boost::this_thread::at_thread_exit()`) a thing that
246 * sets #s_this_thread_global_tl_state_operative to `false` (*inoperative*) strictly before
247 * `boost::thread_specific_ptr` cleanup (`cleanup_func()`s-or-`delete`s + map removal) executes.
248 *
249 * @see #s_this_thread_global_tl_state_operative doc header. It explains why this should be done at that stage:
250 * in `~Global_tl_state()` dtor would be insufficient.
251 */
253
254 /**
255 * Dtor that runs at `thread_local` deinit phase; unknown where among this thread's such deinits this runs;
256 * but it runs before (in Linux at least) the `pthread_` TLS destructor phase.
257 */
259
260 // Data.
261
262 /// The heart of the matter.
264 }; // struct Global_tl_state
265
266 // Data (thread-local).
267
268 /**
269 * A thread-local flag that starts at `true` and becomes `false` to indicate **do not access Global_tl_state**.
270 * In practice: it becomes `false` just-ahead of `boost::thread_specific_ptr`'s cleanup phase in the given
271 * thread, via `boost::this_thread::at_thread_exit()` in Global_tl_state ctor.
272 *
273 * @see Thread_local_ptr doc header impl section. It explains why this should be done at that stage:
274 * in `~Global_tl_state()` dtor would be insufficient. We now restate that same stuff (in slightly
275 * more concrete terms).
276 *
277 * It is important to understand that **do not access Global_tl_state** is not equal to
278 * "`Global_tl_state` is destroyed." Those would be equal if and only if a live `Global_tl_state`'s contents
279 * (Global_tl_state::m_tlp_states_map) always mirror the `boost::thread_specific_ptr` that `*this` is
280 * speeding up. That is almost always the case, but there could be a sneaky exception:
281 *
282 * During cleanup, `boost::thread_specific_ptr` will run the cleanup callback/`delete` and then delete
283 * the cleanup-target from its map. However it won't call `Thread_local_ptr_cache::reset*()`; it has no
284 * idea that is a thing. It won't call `Thread_local_ptr::reset()` either; same thing. So at that point
285 * Global_tl_state::m_tlp_states_map keeps a thing that is no longer in the `tsp`.
286 *
287 * OK, but is that such a crime? Does it really mean that the map diverging from the canonical map as
288 * hypothesized means "do not access Global_tl_state"? In and of itself no; but
289 * actually yes: If another `cleanup_func()` now `.get()`s the earlier entry, that'll forward to
290 * Thread_local_ptr_cache::get(); that will access the stale map and return a pointer to a likely-deleted
291 * (by the earlier entry's cleanup) thing.
292 *
293 * That was hopefully all comprehensible, but it buried the lede: **do not access Global_tl_state**
294 * (this flag = `false`) needs to be defined more conservatively than "`Global_tl_state` exists."
295 * For example... it could be "just set it to `false` always." That is very conservative, but then
296 * this whole contraption (Thread_local_ptr) might as well not exist. So it needs to be `true` through
297 * most of the thread's lifetime -- sometime up to its end stages. Answer: It is to become `false` just
298 * ahead of the `tsp` cleanups running. Until cleanup, by definition, all modifications of
299 * the `tsp` go through Thread_local_ptr methods and will keep Global_tl_state::m_tlp_states_map accurate.
300 * `false` just before then means cleanup phase shall never access `m_tlp_states_map`: It will not diverge,
301 * nor will it be queried. During cleanup it's all-`tsp`, no `*this`.
302 */
303 inline static thread_local bool s_this_thread_global_tl_state_operative = true;
304
305 /// See Global_tl_state doc header.
306 inline static thread_local Global_tl_state s_this_thread_state;
307
308 // Data.
309
310 /// `*this`'s unique ID; see important notes on #per_tlp_key_t.
312}; // class Thread_local_ptr_cache
313
314// Inline implementations.
315
317{
318 // If in doubt about rationale of things here (as opposed to our mere contract): start with Thread_local_ptr::reset().
319
321 {
322 const auto map_it = s_this_thread_state.m_tlp_states_map.find(m_per_tlp_key);
323 if (map_it == s_this_thread_state.m_tlp_states_map.end())
324 {
325 /* Nothing there => return OK by contract. Rationale:
326 * - If new_value truthy: mainstream new-insert case; in insert phase they'll call reset_set(new_value) next.
327 * - If new_value is falsy: no-op case; in insert phase they'll no-op, since they'll see new_value is null.
328 * So nothing to alert them about; hence OK. */
329 return Reset_result::S_OK;
330 }
331 // else if (*this is in map):
332
333 if (map_it->second.m_tl_state_ptr == new_value)
334 {
335 return Reset_result::S_DUPE; // Caller should skip insert phase entirely.
336 }
337 // else if (old_value != new_value): Delete old_value.
338
339 /* Had we called get() for the above 2 checks, we'd have to re-search for key here. Our approach is thus
340 * faster/better (but wordier). */
342 return Reset_result::S_OK;
343 } // if (s_this_thread_global_tl_state_operative)
344 // else if (!s_this_thread_global_tl_state_operative):
345
347} // Thread_local_ptr_cache::reset_release()
348
350{
351 /* To avoid subsequent reshufflings, reserve this-sized map on first (across *all* `*this`es per thread) insert.
352 * It is only for performance, and even so please remember this is *not* the fast-path (the way this whole
353 * contraption is conceived in the first place); get() is the fast-path. Couple other matters:
354 * - We could do this in a Global_tl_state() ctor; that would be prettier but does use some significant
355 * memory even for a thread that only ever get()s (and yields null).
356 * - The way we do it is less pretty and can also do some redundant .reserve()ing -- if
357 * we do empty=>non-empty=>reserve=>empty=>non-empty=> redundant reserve. That said, then it's a quick
358 * no-op, and it's not the fast-path anyway. Plus it is probably unlikely. */
359 constexpr size_t MAP_RESERVED_SIZE = 1024;
360
362 && "By contract: only call us if `reset_release() != INOPERATIVE` and only immediately after that.");
363 assert(new_value && "Null values not allowed; and reset_set() is not to be used for erasing.");
364
366 {
367 s_this_thread_state.m_tlp_states_map.reserve(MAP_RESERVED_SIZE);
368 }
369
371} // Thread_local_ptr_cache::reset_set()
372
373std::optional<void*> Thread_local_ptr_cache::get() const
374{
375 std::optional<void*> result;
376
378 {
379 const auto map_it = s_this_thread_state.m_tlp_states_map.find(m_per_tlp_key);
380 result.emplace((map_it == s_this_thread_state.m_tlp_states_map.end()) ? static_cast<void*>(nullptr)
381 : map_it->second.m_tl_state_ptr);
382 }
383 // else { Leave result blank indicating INOPERATIVE. }
384
385 return result;
386}
387
389{
391 {
393 }
394}
395
396} // namespace flow::util
Internal-use helper for Thread_local_ptr which encapsulates that guy's thread_locally-accessed TLS pa...
Reset_result reset_release(void *new_value)
Phase 1.1 of Thread_local_ptr::reset(): Executes reset(new_value) up-to when one potentially calls cl...
const per_tlp_key_t m_per_tlp_key
*this's unique ID; see important notes on per_tlp_key_t.
std::optional< void * > get() const
Looks up and returns the value last placed via reset_release() + reset_set(), or null if those have n...
static thread_local Global_tl_state s_this_thread_state
See Global_tl_state doc header.
static thread_local bool s_this_thread_global_tl_state_operative
A thread-local flag that starts at true and becomes false to indicate do not access Global_tl_state.
Reset_result
Encodes result of reset_release(), indicating what the calling Thread_local_ptr::reset() should do ne...
@ S_DUPE
*this is operative; key was looked up (X = result, possibly null); X equals new_value; reset() should...
@ S_OK
*this is operative; key was looked up (X = result, possibly null); X does not equal new_value; X,...
@ S_INOPERATIVE
*this is inoperative permanently (we are near thread exit or exit()); do not touch *this further; res...
Unique_id_holder::id_t per_tlp_key_t
64-bit (at worst) key for Tlp_states_map, uniquely generated by Unique_id_holder.
void release()
Our part of Thread_local_ptr::release().
Thread_local_ptr_cache()
Ctor. No apparent effect.
void reset_set(void *new_value)
Phase 3.2 of Thread_local_ptr::reset(): Executes reset(new_value) just-after the update of the canoni...
boost::unordered_flat_map< per_tlp_key_t, Per_tlp_state > Tlp_states_map
The map for a given thread, with a Per_tlp_state per Thread_local_ptr_cache (<=> Thread_local_ptr<....
uint64_t id_t
Raw integer type to uniquely identify a thing. 64-bit width should make overflow extremely hard to re...
Flow module containing miscellaneous general-use facilities that don't fit into any other Flow module...
Encapsulates two things: the thread's Tlp_states_map; and what shall run at thread_local deinit.
Global_tl_state()
Ctor: registers (boost::this_thread::at_thread_exit()) a thing that sets s_this_thread_global_tl_stat...
~Global_tl_state()
Dtor that runs at thread_local deinit phase; unknown where among this thread's such deinits this runs...
Tlp_states_map m_tlp_states_map
The heart of the matter.
The data stored in the thread-local class-wide Tlp_states_map, per originating Thread_local_ptr_cache...
void * m_tl_state_ptr
The argument new_value to reset_set(); never null. get() == nullptr means there is no Per_tlp_state.