Flow-IPC 3.0.0
Flow-IPC project: Full implementation reference.
thread_lcl_pool_lookup.hpp
Go to the documentation of this file.
1/* Flow-IPC: SHM-jemalloc
2 * Copyright (c) 2023 Akamai Technologies, Inc.; and other contributors.
3 * Each commit is copyright by its respective author or author's employer.
4 *
5 * Licensed under the MIT License:
6 *
7 * Permission is hereby granted, free of charge, to any person obtaining a copy
8 * of this software and associated documentation files (the "Software"), to deal
9 * in the Software without restriction, including without limitation the rights
10 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 * copies of the Software, and to permit persons to whom the Software is
12 * furnished to do so, subject to the following conditions:
13 *
14 * The above copyright notice and this permission notice shall be included in
15 * all copies or substantial portions of the Software.
16 *
17 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 * THE SOFTWARE. */
24
25/// @file
26#pragma once
27
29#include "ipc/shm/arena_lend/shm_pool.hpp"
31#include <flow/util/thread_lcl.hpp>
32#include <flow/util/util.hpp>
33#include <flow/util/stat/stat_set_list.hpp>
34#include <flow/util/stat/stat_set.hpp>
35#include <boost/container/flat_map.hpp>
36#include <boost/unordered/unordered_flat_map.hpp>
37#include <type_traits>
38#include <cstdint>
39
41{
42
43// Types.
44
45/**
46 * Value type in the reverse-lookup map: identifies the pool and, when `LOOKUP_CAN_FAIL` is `true` for the
47 * containing lookup structure, also carries the pool size so that a range-check can determine whether a
48 * given vaddr actually falls inside the pool (as opposed to merely being above its base).
49 */
51{
52 // Data.
53
54 /// Pool identifier.
56 /**
57 * Pool size in bytes, or 0 when the lookup context guarantees that every address is in some pool
58 * (`LOOKUP_CAN_FAIL == false`), rendering the size check unnecessary.
59 */
60 Shm_pool::size_t m_sz_or_0 = 0;
61};
62
63/**
64 * Global stats singleton for either Owner_pool_lookup_global_stats or Borrower_pool_lookup_global_stats, per
65 * SHM-provider (as identified by `Shm_arena` type).
66 *
67 * Stat-consumers: call stats() to read immutable Owner_pool_lookup_global_stats or Borrower_pool_lookup_global_stats;
68 * stats_reset() to reset same.
69 *
70 * Stat-updaters: call stats_mutable() to update same.
71 *
72 * @tparam Shm_arena_t
73 * A thing that discriminates between SHM-providers; by convention the arena type (such as jemalloc::Ipc_arena).
74 * Its API is not accessed.
75 * @tparam OWNER_ELSE_BORROWER
76 * `true` if tracking owner-side activity; #Stats is stat::Owner_pool_lookup_global_stats.
77 * `false` if borrower; stat::Borrower_pool_lookup_global_stats.
78 */
79template<typename Shm_arena_t, bool OWNER_ELSE_BORROWER>
81{
82public:
83 // Constants.
84
85 /// Constant for template parameter.
86 static constexpr bool S_OWNER_ELSE_BORROWER = OWNER_ELSE_BORROWER;
87
88 // Types.
89
90 /// Short-hand for the template parameter.
91 using Shm_arena = Shm_arena_t;
92
93 /// The actual stat-set type.
94 using Stats = std::conditional_t<S_OWNER_ELSE_BORROWER,
97 // Methods.
98
99 /**
100 * Returns reference-to-`const` stat-set, carrying information that is global but per-template-instance-type.
101 * @return See above. See also doc header of the #Stats alias's target type.
102 */
103 static const Stats& stats();
104
105 /**
106 * Mutable version of stats(), for stat-updating.
107 * @return See above.
108 */
109 static Stats* stats_mutable();
110
111 /// Resets stats(). The formal meaning of a reset is discussed in `flow::util::stat` doc header.
112 static void stats_reset();
113}; // class Pool_lookup_global_stats
114
115/**
116 * `Thread_local_pool_lookup_rev<R>.s_registry`, where `R` is a distinct pool-repository type,
117 * enables high-perf reverse-lookup from a vaddr to the containing SHM-pool's ID and
118 * offset in bytes from the pool-base vaddr.
119 *
120 * As far as we know, if one needs multithreaded such lookup with high performance, then whether owner-side or
121 * borrower-side one should use Thread_local_pool_lookup_rev. Note that `Thread_local_pool_lookup_rev<R1>`
122 * and `Thread_local_pool_lookup_rev<R2>` are completely independent, if `R1` and `R2` are different types.
123 *
124 * @see This is a key building block for the rev-lookup operations of Owner_shm_pool_repository (and
125 * Shm_pool_offset_ptr_data -- for Shm_pool_offset_ptr -- operating against it, owner-side) and of
126 * session::shm::arena_lend::detail::Borrower_shm_pool_collection_repository (ditto but borrower-side).
127 * In both cases this is done indirectly via Shm_pool_repo_lookup_core_rev which uses us directly.
128 * The rationale of separating us (Thread_local_pool_lookup_rev) from Shm_pool_repo_lookup_core_rev is the
129 * analogous nature of us and our forward-lookup cousin Thread_local_pool_lookup_fwd.
130 *
131 * ### How to use ###
132 * To actually perform the reverse-lookup operation, whose performance we are optimizing, use:
133 * - `Thread_local_pool_lookup_rev<Repository>::s_registry.this_thread_state()->lookup(vaddr, ...)`.
134 *
135 * The user has a rigid responsibility for loading the canonical, and complete, set of SHM-pool base vaddrs and
136 * their respective #pool_id_t values. This consists of two operations, and you must help us perform each:
137 * - *Pull*: A `*this` is auto-created in new thread Ti the first time it is needed.
138 * Therefore the new `*this` *pulls* the current canonical state from somewhere that you have it.
139 * - *Push*: Your current canonical state is updated (e.g., memory-manager created new pool).
140 * Therefore you *push* that state, wherever you've got it, into *every* extant `*this`.
141 *
142 * ### How to use: pull ###
143 * During the per-thread creation of a `*this`, the following expression shall be invoked on-demand:
144 * - `Repository::get_instance().tl_copy_rev_pull_pools(&M)`, where
145 * - `M` is #Map (vaddr -> Pool_by_base), and
146 * - that expression shall load `M` with the complete current information about base-vaddrs and pool info.
147 * - A central (1-1 with the type #Repository) mutex `Mtx` shall be locked;
148 * and it is the same mutex that shall be locked during a push (see below).
149 *
150 * ### How to use: push ###
151 * When the canonical such structure is modified in repo #Repository (addition,
152 * removal, init), that thread must do the following (certainly before any relevant subsequent lookup() calls):
153 * - `Thread_local_pool_lookup_rev<Repository>::s_registry.while_locked() {...}`.
154 * - Inside the `{...}`, obtain and loop-through each thread's extant thread-local
155 * Thread_local_pool_lookup_rev cache: `state_per_thread` from `.while_locked()` is a map whose
156 * keys are each a `Thread_local_pool_lookup_rev<Repository>`. (The values are some metadata like thread ID
157 * or what-not.)
158 * - For each of those: `cache.push_pools(M)`, where `M` is the complete new mapping.
159 * - The mutex that shall be locked inside the `{...}` shall be the same mutex `Mtx` used during any pull
160 * (see above).
161 *
162 * ### Design / Impl ###
163 * The basic lookup algorithm, even in one thread only, is that one needs a sorted-by-base-vaddr map from
164 * base-vaddr to pool ID, and with logarithmic time complexity lookup() shall perform a binary-search
165 * (as in `std::map::upper_bound()`) lookup of a nearby (versus the input `void*`) base-vaddr plus an
166 * adjustment of the resulting iterator. (That gets the pool ID; the offset is just the address minus the
167 * located base-vaddr.)
168 *
169 * For multithreaded access, the naive approach (full synchronization via one central mutex) exhibits terrible
170 * lock contention under high load. Therefore we looked for something involving a thread-local lookup().
171 * Flow's `Thread_local_state_registry` provides TL semantics; we expose a global #s_registry of that type;
172 * so `s_registry::this_thread_state()->lookup()` is how one would perform the lookup.
173 *
174 * In short: a given `*this` shall contain an up-to-date copy of the canonical set of bases and corresponding pool info.
175 * Therefore one must copy-over the canonical such map into the thread-local #m_pools_sorted_by_base
176 * *for each thread*, whenever said canonical map is updated (in whatever *pusher thread* happens to be
177 * performing that update).
178 *
179 * Naturally this creates the need for locking -- whose perf characteristics require scrutiny. The main critical
180 * mutex is a *per-thread* mutex (hence a non-`static` member, #m_pools_sorted_by_base_mutex). It is to be locked:
181 * - (Frequent) In end-user thread: lookup() -- around the lookup of #m_pools_sorted_by_base.
182 * - (Rare) In pusher thread: push_pools() -- around the replacement of #m_pools_sorted_by_base.
183 * - Since, if one assumes for simplicity of exposition that there's only one update happening at a time,
184 * this implies *at most* 2 threads contending for the lock, and usually only 1, this is perf-acceptable.
185 *
186 * @note That says "creates the need for locking," but it is possible to eliminate the (frequently accessed)
187 * mutex as well which would get back some more perf. See "Potential design improvement."
188 *
189 * There is another important lock involved. Per `flow::util::Thread_local_state_registry`, when looping
190 * over extant thread-local objects (`*this`es in our case) -- a key service of that utility module -- one
191 * shall lock a built-in central mutex via `Thread_local_state_registry.while_locked(F)`, where `F()` contains
192 * the code to copy-over the canonical map into individual `*this`es. Hence the directive to use
193 * `state_per_thread` in `F(state_per_thread)` to obtain the `*this`es.
194 *
195 * @note Tip: Since it is likely that the user code wants to protect against concurrent updates to the canonical map,
196 * it can take advantage of the mutex mentioned in the previous paragraph. That is `F()` given to
197 * `while_locked()` can perform the update of the canonical map first as well.
198 *
199 * Thus, during an update push_pools() code-flow, for a given `*this`, the central lock is locked, then the per-thread
200 * lock, then #m_pools_sorted_by_base is updated, then the locks are unlocked (in opposite order).
201 *
202 * During a pull code-flow, only the central lock is locked, then #m_pools_sorted_by_base is initialized (before
203 * being available to the user).
204 *
205 * During the lookup() code-flow, only the per-thread lock is locked.
206 *
207 * There is no deadlock.
208 *
209 * ### Design / Impl: Potential design improvement ###
210 * This little section is basically a to-do:
211 *
212 * @todo The algorithm used in Thread_local_pool_lookup_rev (owner-side and borrower-side), and similarly
213 * Thread_local_pool_lookup_fwd (borrower-side only), that relies on a frequently locked, but ~never contended,
214 * mutex can be improved further by instead using some one-way atomic signaling. Please read the commentary
215 * after this to-do in the code.
216 *
217 * The existing algorithm is nice. Along the frequently-called fast-path, namely in lookup(), a mutex is
218 * [un]locked every single time, but there's almost never another thread contending for it (and even then at most 1).
219 * So the slightly annoying cost that's still there = the ~uncontended lock+unlock ops. That's quick but may well
220 * be comparable to the map-lookup itself. We do optimize the map-lookup, and it'd be nice to cut down the [un]lock
221 * latency too. That said, unlike w/r/t the overall larger problem -- where we had real-world evidence that, say,
222 * doing a straight fully contended central-mutex [un]lock caused massive perf problems -- this smaller deficit is
223 * reasoned from just the code, not any field observations. Even so the improvement might be simple enough to
224 * "just" apply preemptively. To wit, for a given thread that would lookup():
225 *
226 * Currently there is first the *push* (lock { update thread's map }), second the *use* in `lookup()`
227 * (lock { use map for lookup }). In short: This could be turned into a one-way
228 * hand-off/produce-consume type of thing. The product is the thread's *pending* map copy.
229 * The producer is the push code: make the copy and set (this thread's) *pending* pointer to point to it, using an
230 * atomic release-exchange op. The (lazy) consumer is `lookup()`: no more lock op; but atomically consume
231 * (acquire-load / possibly acquire-exchange) the *pending* pointer; typically no change; but otherwise free
232 * the *current* map (of which it is the only user) and update *current*. Then use the map which is the *current*
233 * pointee.
234 *
235 * One technicality about the above we have to zoom into, as we don't want to mislead the implementer:
236 * The `lookup()` operation on the *pending* pointer is tempting to write as simply an atomic exchange with null:
237 * if we pop null out then no change; otherwise what we've got should become *current*. Slick and simple,
238 * but the existing mutex-lock op is already itself a read-modify-write (granted, there's the unlock too later,
239 * adding more cycles still). We should do better: separate into 1-2 ops instead. First do an acquire-load (no
240 * potential write). If null (fast-path)... done (no update needed); cycles nicely saved! If not null, *then*
241 * do the acquire-exchange-with-null after all (but it's the infrequent/slow-path). The thing the exchange op spits
242 * out -- cannot be null but might be different from whatever the earlier acquire-load probe yielded -- is to
243 * become *current*.
244 *
245 * In general, if this per-thread *pending* pointer is only ever modified via atomic-exchange, a clean algorithm is
246 * natural and apparent. Whichever module (either the push-code or `lookup()`) pops out a non-null ptr value becomes
247 * responsible for, at some point subsequently, freeing the pointee map. Push-code would free immediately: it
248 * means another central update happened before this thread got to consuming the preceding one's map. `lookup()`
249 * module would free it once *current* map is itself replaced later.
250 *
251 * ### Stats -- If #S_LOOKUP_CAN_FAIL is `true` only ###
252 * The object at `static` Pool_lookup_global_stats<Shm_arena, true>::stats() -- a global, per-`decltype(*this)`
253 * arena_lend::stat::Owner_pool_lookup_global_stats -- contains some interesting stats as updated by every `*this`.
254 *
255 * To reset this stat-set, use Pool_lookup_global_stats<Shm_arena, true>::stats_reset().
256 *
257 * ### Stats -- If #S_LOOKUP_CAN_FAIL is `false` only ###
258 * The object at `static` Pool_lookup_global_stats<Shm_arena, false>::stats() -- a global, per-`decltype(*this)`
259 * arena_lend::stat::Borrower_pool_lookup_global_stats -- contains some interesting stats as updated by every `*this`.
260 *
261 * To reset this stat-set, use Pool_lookup_global_stats<Shm_arena, false>::stats_reset().
262 *
263 * @tparam Repository_t
264 * See above. Formally(ish) there are two requirements/properties. 1, if and only if
265 * `is_same_v<R1, R2> == false` then `Thread_local_pool_lookup_rev<R1, ...>` and
266 * `Thread_local_pool_lookup_rev<R2, ...>` are independent registries; otherwise they are the same registry.
267 * 2, `Repository_t::get_instance().tl_copy_rev_pull_pools()` must exist and do what is
268 * explained above.
269 * @tparam LOOKUP_CAN_FAIL
270 * If `true`, lookup() may fail to find an address in any pool (yields `pool_id = 0` in that case).
271 * This is needed on the owner side, where an `Shm_pool_offset_ptr` with `CAN_STORE_RAW_PTR == true`
272 * may call `from_address()` on a non-SHM (stack/heap) address.
273 * If `false`, every address is assumed to reside in some known pool (borrower side), and lookup()
274 * has undefined behavior on a miss; pool sizes are not stored.
275 * @tparam Shm_arena_t
276 * See Pool_lookup_global_stats.
277 */
278template<typename Repository_t, bool LOOKUP_CAN_FAIL, typename Shm_arena_t>
280{
281public:
282 // Types.
283
284 /// Short-hand for the template parameter.
285 using Repository = Repository_t;
286 /// Short-hand for the template parameter.
287 using Shm_arena = Shm_arena_t;
288 /// Mirrors the template parameter; `true` if lookup() can legitimately fail to find the containing pool.
289 static constexpr bool S_LOOKUP_CAN_FAIL = LOOKUP_CAN_FAIL;
290 /// Short-hand for pool-offset type (byte offset from pool base).
291 using pool_offset_t = Shm_pool::size_t;
292
293 /**
294 * Reverse-lookup map type (base vaddr -> Pool_by_base). Used internally but exposed to our direct user via
295 * the push and pull APIs.
296 *
297 * ### Internally: Rationale for chosen type ###
298 * Used for #m_pools_sorted_by_base; is a `flat_map` (internally: sorted contiguous vector) rather than `std::map`
299 * (red-black tree). This is ideal here: lookup() -- the perf-critical hot path -- performs `upper_bound()` on
300 * contiguous memory (cache-friendly binary search) instead of pointer-chasing through scattered tree nodes.
301 *
302 * The downside of `flat_map` -- O(n) insertion -- is low-impact: the thread-local copy is never incrementally
303 * modified; it is only replaced wholesale in push_pools() (centrally when pool added/removed) and initialized
304 * wholesale in the pull path (once per thread). Both are off the fast-path.
305 */
306 using Map = boost::container::flat_map<void*, Pool_by_base>;
307
308 // Constructors/destructor.
309
310 /**
311 * Constructs this thread-local cache by pulling the current canonical pool state from #Repository.
312 * Called automatically by #s_registry when a new thread first accesses `this_thread_state()`.
313 *
314 * `s_registry.while_locked()` is in effect at the time of this call (per
315 * `Thread_local_state_registry::this_thread_state()` contract).
316 *
317 * @warning It is public due to `Thread_local_state_registry` requirements only. Do not call directly.
318 * See class doc header "How to use: pull" section however.
319 */
321
322 /// Destructor: runs at thread exit.
324
325 // Methods.
326
327 /**
328 * Performs the perf-critical reverse lookup: given a vaddr, determines the containing pool's ID and the
329 * byte offset of the address from that pool's base.
330 *
331 * If `S_LOOKUP_CAN_FAIL == false`, `address` must reside within a known pool (else undefined behavior).
332 * If `S_LOOKUP_CAN_FAIL == true`, `address` may be outside any pool; in that case `pool_id` is set to 0
333 * and `pool_offset` is left untouched.
334 *
335 * @param address
336 * The vaddr to look up.
337 * @param pool_id
338 * On return, set to the ID of the pool containing `address`; or 0 if not found
339 * (only when `S_LOOKUP_CAN_FAIL`).
340 * @param pool_offset
341 * On return, set to `address` minus the base vaddr of the containing pool; untouched if not found
342 * (<=> `pool_id == 0` on return).
343 */
344 void lookup(const void* address, pool_id_t& pool_id, pool_offset_t& pool_offset) const;
345
346 /**
347 * Replaces this thread-local sorted map with a new canonical snapshot.
348 *
349 * Called from the pusher thread (not necessarily the thread owning this `*this`).
350 * `s_registry.while_locked()` shall be in effect at the time of this call.
351 *
352 * See class doc header "How to use: push" section.
353 *
354 * @param pools_sorted_by_base
355 * The complete new mapping (pool-base vaddr -> Pool_by_base); moved-from before return.
356 */
357 void push_pools(Map&& pools_sorted_by_base);
358
359 // Data.
360
361 /**
362 * Global registry of per-thread `*this` instances. See class doc header for the pull/push protocol
363 * using `this_thread_state()`, `while_locked()`.
364 */
365 static flow::util::Thread_local_state_registry<Thread_local_pool_lookup_rev> s_registry;
366
367private:
368 // Data.
369
370 /// Per-thread mutex protecting #m_pools_sorted_by_base. Contended by at most 2 threads (local lookup versus pusher).
371 mutable flow::util::Mutex_non_recursive m_pools_sorted_by_base_mutex;
372
373 /**
374 * Thread-local copy of the canonical sorted map: pool-base vaddr -> Pool_by_base. See #Map doc and class
375 * doc header.
376 */
378}; // class Thread_local_pool_lookup_rev
379
380/**
381 * Analogous to Thread_local_pool_lookup_rev but for forward lookup: pool ID -> base vaddr. Used borrower-side
382 * only (read on as to why not owner-side).
383 *
384 * As far as we know, if one needs multithreaded such lookup with high performance, then
385 * borrower-side one should use Thread_local_pool_lookup_fwd. However owner-side one can use a
386 * straight `static thread_local` map from pool ID to `void*`, updating it lazily on-demand on cache miss (letting
387 * outdated pool entries negligibly leak). Such a solution is insufficient for borrower-side lookup, as
388 * it is possible a pool is mapped, unmapped, then mapped to a different base vaddr -- but a thread's cache
389 * might (forever) store the now-incorrect base-vaddr. Hence use Thread_local_pool_lookup_fwd.
390 *
391 * The API and impl are the same: thread-local caches in a global #s_registry, with the same pull/push
392 * protocol and the same per-thread locking strategy. See Thread_local_pool_lookup_rev class doc header for
393 * the full design discussion; only the differences are noted here:
394 * - The cached data structure, while still a map, is simply looked-up directly: pool ID in, base-vaddr out.
395 * The chosen type for #Map may be of interest (see `Map` doc header).
396 * - The pull callback is `Repository::get_instance().tl_copy_fwd_pull_pools()` (not `..._rev_...`).
397 *
398 * ### Stats ###
399 * The object at `static` Pool_lookup_global_stats<Shm_arena, false>::stats() -- a global, per-`decltype(*this)`
400 * arena_lend::stat::Borrower_pool_lookup_global_stats -- contains some interesting stats as updated by every `*this`.
401 *
402 * To reset this stat-set, use Pool_lookup_global_stats<Shm_arena, false>::stats_reset().
403 *
404 * @tparam Repository_t
405 * Same concept as for Thread_local_pool_lookup_rev.
406 * `Repository_t::get_instance().tl_copy_fwd_pull_pools()` must exist and behave analogously
407 * to the reverse counterpart.
408 * @tparam Shm_arena_t
409 * See Pool_lookup_global_stats.
410 */
411template<typename Repository_t, typename Shm_arena_t>
413{
414public:
415 // Types.
416
417 /// Short-hand for the template parameter.
418 using Repository = Repository_t;
419 /// Short-hand for the template parameter.
420 using Shm_arena = Shm_arena_t;
421 /// Short-hand for pool-offset type (byte offset from pool base).
422 using pool_offset_t = Shm_pool::size_t;
423
424 /**
425 * Forward-lookup map type (pool ID -> base vaddr). Used internally but exposed to our direct user via
426 * the push and pull APIs.
427 *
428 * ### Internally: Rationale for chosen type ###
429 * As the Thread_local_pool_lookup_rev::Map cousin uses `flat_map` (sorted contiguous vector) for
430 * cache-friendly `upper_bound()`, we analogously use `unordered_flat_map` (open-addressing hash table) for
431 * cache-friendly `find()`. Same rationale.
432 */
433 using Map = boost::unordered_flat_map<pool_id_t, void*>;
434
435 // Constructors/destructor.
436
437 /// Analogous to Thread_local_pool_lookup_rev::Thread_local_pool_lookup_rev(). Reminder: Do not call directly.
439
440 /// Destructor: runs at thread exit.
442
443 // Methods.
444
445 /**
446 * Performs the forward lookup: given a pool ID, returns the pool's base vaddr; if pool ID is invalid undefined
447 * behavior ensues.
448 *
449 * @see lookup_safe() for an alternative without the aforementioned undefined-behavior.
450 *
451 * @param pool_id
452 * The pool ID to look up.
453 * @return The base vaddr of the pool identified by `pool_id`.
454 */
455 void* lookup(pool_id_t pool_id) const;
456
457 /**
458 * Identical to lookup() but returns null if `pool_id` is not a registered pool (never registered or registered
459 * but removed).
460 *
461 * @param pool_id
462 * The pool ID to look up.
463 * @return The base vaddr of the pool identified by `pool_id`; or null.
464 */
465 void* lookup_safe(pool_id_t pool_id) const;
466
467 /**
468 * Analogous to Thread_local_pool_lookup_rev::push_pools() but replaces the forward-lookup hash map.
469 *
470 * @param pool_bases_by_id
471 * The complete new mapping (pool ID -> base vaddr); moved-from on return.
472 */
473 void push_pools(Map&& pool_bases_by_id);
474
475 // Data.
476
477 /**
478 * Global registry of per-thread `*this` instances.
479 * @see Thread_local_pool_lookup_rev::s_registry -- same protocol applies.
480 */
481 static flow::util::Thread_local_state_registry<Thread_local_pool_lookup_fwd> s_registry;
482
483private:
484 // Data.
485
486 /// Per-thread mutex protecting #m_pool_bases_by_id. Contended by at most 2 threads (local lookup versus pusher).
487 mutable flow::util::Mutex_non_recursive m_pool_bases_by_id_mutex;
488
489 /// Thread-local copy of the canonical hash map: pool ID -> base vaddr. See #Map doc and class doc header.
491}; // class Thread_local_pool_lookup_fwd
492
493// Template static initializers.
494
495template<typename Repository_t, bool LOOKUP_CAN_FAIL, typename Shm_arena_t>
496flow::util::Thread_local_state_registry<Thread_local_pool_lookup_rev<Repository_t, LOOKUP_CAN_FAIL, Shm_arena_t>>
498 {nullptr, "Thread_local_pool_lookup_rev"};
499
500template<typename Repository_t, typename Shm_arena_t>
501flow::util::Thread_local_state_registry<Thread_local_pool_lookup_fwd<Repository_t, Shm_arena_t>>
502 Thread_local_pool_lookup_fwd<Repository_t, Shm_arena_t>::s_registry{nullptr, "Thread_local_pool_lookup_fwd"};
503
504// Template implementations: Thread_local_pool_lookup_rev.
505
506template<typename Repository_t, bool LOOKUP_CAN_FAIL, typename Shm_arena_t>
508{
509 using flow::util::stat::fetch_add;
510 using flow::util::stat::update_hi_wmark;
511
512 // s_registry.while_locked() is in effect (by Thread_local_state_registry::this_thread_state() contract).
513
514 // No need to lock m_pools_sorted_by_base_mutex yet: we have not been returned to user.
515
516 Repository::get_instance().tl_copy_rev_pull_pools(&m_pools_sorted_by_base);
517
518 { // Stats.
520 update_hi_wmark(&stats.m_pool_rev_lookup_tl_cache_count_hi_wmark,
521 fetch_add(&stats.m_pool_rev_lookup_tl_cache_count, 1) + 1);
522 }
523}
524
525template<typename Repository_t, bool LOOKUP_CAN_FAIL, typename Shm_arena_t>
527{
528 using flow::util::stat::fetch_sub;
529
530 { // Stats.
532 ->m_pool_rev_lookup_tl_cache_count), 1);
533 }
534}
535
536template<typename Repository_t, bool LOOKUP_CAN_FAIL, typename Shm_arena_t>
538 pool_id_t& pool_id,
539 pool_offset_t& pool_offset) const
540{
541 // Lock with minimal contention (see class doc header Impl section).
542 flow::util::Lock_guard<decltype(m_pools_sorted_by_base_mutex)> lock{m_pools_sorted_by_base_mutex};
543
544 /* It would be typical in the rest of the code base to add assert(s)/guards/things like that.
545 * However we choose to skip past anything that might take cycles in production -- where it is not all that uncommon
546 * to keep assertions enabled. We're playing a little chicken here on purpose; the perf here has been
547 * shown under load to be of high impact. */
548
549 /* upper_bound() returns the first element with base > address. The candidate pool (if any) is the one
550 * just before that. Unlike with lower_bound(), there is no exact-match special case: always `--it`. */
551 auto it = m_pools_sorted_by_base.upper_bound(const_cast<void*>(address));
552
553 if constexpr(S_LOOKUP_CAN_FAIL)
554 {
555 if (it == m_pools_sorted_by_base.begin())
556 {
557 pool_id = 0;
558 return;
559 }
560 }
561 // else: Cannot be begin() by contract (address is in some pool).
562
563 --it;
564
565 const auto& pool_data = it->second;
566 const auto addr_ptr = reinterpret_cast<uintptr_t>(address);
567 const auto pool_base = reinterpret_cast<uintptr_t>(it->first);
568
569 if constexpr(S_LOOKUP_CAN_FAIL)
570 {
571 /* Check that address actually falls within this pool's [base, base + size) range.
572 * If not, it's in the gap between pools (or past the last pool). */
573 if (addr_ptr >= (pool_base + pool_data.m_sz_or_0))
574 {
575 pool_id = 0;
576 return;
577 }
578 }
579
580 pool_id = pool_data.m_id;
581 pool_offset = addr_ptr - pool_base;
582} // Thread_local_pool_lookup_rev::lookup()
583
584template<typename Repository_t, bool LOOKUP_CAN_FAIL, typename Shm_arena_t>
586{
587 // We are potentially *not* in our thread-local thread.
588
589 // s_registry.while_locked() is in effect.
590
591 // Must also lock this against at least lookup().
592 flow::util::Lock_guard<decltype(m_pools_sorted_by_base_mutex)> lock{m_pools_sorted_by_base_mutex};
593
594 m_pools_sorted_by_base = std::move(pools_sorted_by_base);
595}
596
597// Template implementations: Thread_local_pool_lookup_fwd.
598
599template<typename Repository_t, typename Shm_arena_t>
601{
602 using flow::util::stat::fetch_add;
603 using flow::util::stat::update_hi_wmark;
604
605 // s_registry.while_locked() is in effect.
606
607 // No need to lock m_pool_bases_by_id_mutex yet: we have not been returned to user.
608
609 Repository::get_instance().tl_copy_fwd_pull_pools(&m_pool_bases_by_id);
610
611 { // Stats.
613 update_hi_wmark(&stats.m_pool_fwd_lookup_tl_cache_count_hi_wmark,
614 fetch_add(&stats.m_pool_fwd_lookup_tl_cache_count, 1) + 1);
615 }
616}
617
618template<typename Repository_t, typename Shm_arena_t>
620{
621 using flow::util::stat::fetch_sub;
622
623 { // Stats.
624 fetch_sub(&(Pool_lookup_global_stats<Shm_arena, false>::stats_mutable()->m_pool_fwd_lookup_tl_cache_count), 1);
625 }
626}
627
628template<typename Repository_t, typename Shm_arena_t>
630{
631 // Lock with minimal contention.
632 flow::util::Lock_guard<decltype(m_pool_bases_by_id_mutex)> lock{m_pool_bases_by_id_mutex};
633
634 // As in _rev::lookup() for same reason... intentionally not checking against end()/assert()ing/etc.
635
636 return m_pool_bases_by_id.find(pool_id)->second;
637}
638
639template<typename Repository_t, typename Shm_arena_t>
641{
642 // Lock with minimal contention.
643 flow::util::Lock_guard<decltype(m_pool_bases_by_id_mutex)> lock{m_pool_bases_by_id_mutex};
644
645 const auto it = m_pool_bases_by_id.find(pool_id);
646 return (it == m_pool_bases_by_id.end()) ? static_cast<void*>(nullptr)
647 : it->second;
648}
649
650template<typename Repository_t, typename Shm_arena_t>
652{
653 // We are potentially *not* in our thread-local thread.
654
655 // s_registry.while_locked() is in effect.
656
657 // Must also lock this against at least lookup().
658 flow::util::Lock_guard<decltype(m_pool_bases_by_id_mutex)> lock{m_pool_bases_by_id_mutex};
659
660 m_pool_bases_by_id = std::move(pool_bases_by_id);
661}
662
663// Template implementations: Pool_lookup_global_stats.
664
665template<typename Shm_arena_t, bool OWNER_ELSE_BORROWER>
668{
669 /* Rationale for using the Flow Global_stats facility (as opposed to just shoving a `public: static`
670 * Stats member in the class): That would have been fine too, but then one would have to worry about
671 * static-init order. Could do an on-demand singleton ourselves, but Global_stats provides
672 * on-demand static-init (and properly ordered deinit courtesy of C++ rules), so we use it to reduce
673 * our impl burden here. */
674 return flow::util::stat::Global_stats<Pool_lookup_global_stats, Stats>::get().stats_default();
675}
676
677template<typename Shm_arena_t, bool OWNER_ELSE_BORROWER>
680{
681 return &(flow::util::stat::Global_stats<Pool_lookup_global_stats, Stats>::get().stats_mutable_default());
682}
683
684template<typename Shm_arena_t, bool OWNER_ELSE_BORROWER>
686{
687 flow::util::stat::Global_stats<Pool_lookup_global_stats, Stats>::get().reset();
688}
689
690} // namespace ipc::shm::arena_lend::detail
Global stats singleton for either Owner_pool_lookup_global_stats or Borrower_pool_lookup_global_stats...
static constexpr bool S_OWNER_ELSE_BORROWER
Constant for template parameter.
std::conditional_t< S_OWNER_ELSE_BORROWER, arena_lend::stat::Owner_pool_lookup_global_stats, arena_lend::stat::Borrower_pool_lookup_global_stats > Stats
The actual stat-set type.
static void stats_reset()
Resets stats(). The formal meaning of a reset is discussed in flow::util::stat doc header.
static const Stats & stats()
Returns reference-to-const stat-set, carrying information that is global but per-template-instance-ty...
Shm_arena_t Shm_arena
Short-hand for the template parameter.
static Stats * stats_mutable()
Mutable version of stats(), for stat-updating.
Analogous to Thread_local_pool_lookup_rev but for forward lookup: pool ID -> base vaddr.
Repository_t Repository
Short-hand for the template parameter.
Shm_arena_t Shm_arena
Short-hand for the template parameter.
void * lookup(pool_id_t pool_id) const
Performs the forward lookup: given a pool ID, returns the pool's base vaddr; if pool ID is invalid un...
Map m_pool_bases_by_id
Thread-local copy of the canonical hash map: pool ID -> base vaddr. See Map doc and class doc header.
Thread_local_pool_lookup_fwd()
Analogous to Thread_local_pool_lookup_rev::Thread_local_pool_lookup_rev(). Reminder: Do not call dire...
void * lookup_safe(pool_id_t pool_id) const
Identical to lookup() but returns null if pool_id is not a registered pool (never registered or regis...
boost::unordered_flat_map< pool_id_t, void * > Map
Forward-lookup map type (pool ID -> base vaddr).
void push_pools(Map &&pool_bases_by_id)
Analogous to Thread_local_pool_lookup_rev::push_pools() but replaces the forward-lookup hash map.
static flow::util::Thread_local_state_registry< Thread_local_pool_lookup_fwd > s_registry
Global registry of per-thread *this instances.
Shm_pool::size_t pool_offset_t
Short-hand for pool-offset type (byte offset from pool base).
flow::util::Mutex_non_recursive m_pool_bases_by_id_mutex
Per-thread mutex protecting m_pool_bases_by_id. Contended by at most 2 threads (local lookup versus p...
Thread_local_pool_lookup_rev<R>.s_registry, where R is a distinct pool-repository type,...
Shm_pool::size_t pool_offset_t
Short-hand for pool-offset type (byte offset from pool base).
boost::container::flat_map< void *, Pool_by_base > Map
Reverse-lookup map type (base vaddr -> Pool_by_base).
void lookup(const void *address, pool_id_t &pool_id, pool_offset_t &pool_offset) const
Performs the perf-critical reverse lookup: given a vaddr, determines the containing pool's ID and the...
flow::util::Mutex_non_recursive m_pools_sorted_by_base_mutex
Per-thread mutex protecting m_pools_sorted_by_base. Contended by at most 2 threads (local lookup vers...
static constexpr bool S_LOOKUP_CAN_FAIL
Mirrors the template parameter; true if lookup() can legitimately fail to find the containing pool.
Map m_pools_sorted_by_base
Thread-local copy of the canonical sorted map: pool-base vaddr -> Pool_by_base.
Shm_arena_t Shm_arena
Short-hand for the template parameter.
Thread_local_pool_lookup_rev()
Constructs this thread-local cache by pulling the current canonical pool state from Repository.
void push_pools(Map &&pools_sorted_by_base)
Replaces this thread-local sorted map with a new canonical snapshot.
static flow::util::Thread_local_state_registry< Thread_local_pool_lookup_rev > s_registry
Global registry of per-thread *this instances.
Repository_t Repository
Short-hand for the template parameter.
Segregated private stuff for ipc::shm::arena_lend.
uint32_t pool_id_t
Pool ID type.
Value type in the reverse-lookup map: identifies the pool and, when LOOKUP_CAN_FAIL is true for the c...
Shm_pool::size_t m_sz_or_0
Pool size in bytes, or 0 when the lookup context guarantees that every address is in some pool (LOOKU...
Analogous subject matter to Owner_pool_lookup_global_stats but borrower-side.
Similar mission to Owner_pool_stats but covers information that, by nature, is not per-arena but rath...