Flow-IPC 3.0.0
Flow-IPC project: Full implementation reference.
use_count_registry.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
28#include "ipc/common.hpp"
32#include <flow/util/util.hpp>
33#include <boost/interprocess/sync/mutex_family.hpp>
34#include <boost/interprocess/offset_ptr.hpp>
35#include <boost/array.hpp>
36
38{
39
40/**
41 * A boost.ipc-compliant memory-algorithm suitable for use in a bipc_ext::Sparse_managed_shm
42 * (which is a sparsely-committing equivalent of `boost::interprocess::basic_managed_shared_memory`)
43 * whose mission is narrow: to allocate N-byte use-counts in Lend_tracker_pool.
44 * (N is a compile-time constant, namely #S_ALLOC_SZ. Therefore in particular a `static_assert()` elsewhere guarantees
45 * that `sizeof(Lend_tracker_pool::Atomic_use_ct::value_type) == Use_count_registry::S_ALLOC_SZ`. When thinking about
46 * this generically, we recommend imagining N is either 1 or 4 so as to ballpark practical implications.)
47 *
48 * That is, it essentially works only for `allocate(S_ALLOC_SZ)` (N-byte allocations) and corresponding
49 * deallocate() calls but is internally optimized to store such N-byte individual objects tightly and to use a
50 * custom bitmap-based algorithm to find free slots quite quickly (and return them even more quickly).
51 *
52 * @note We might refer here and elsewhere to a bipc_ext::Sparse_managed_shm. See its docs for more info, but
53 * for convenience the short version is: It is essentially a
54 * `boost::interprocess::basic_managed_shared_memory`, except that (as a RAM-saving measure on Lend_tracker_pool's
55 * part) it only sparsely commits its full size. That is only pages that actually get touched (mainly by
56 * our allocate()) are RAM-committed (<=> that RAM is taken <=> cannot be used by others until the whole thing
57 * is removed). `Sparse_managed_shm` = on-demand-RAM-committing/sparse `basic_managed_shared_memory`.
58 * @warning While Use_count_registry can be used as the memory-algorithm for the vanilla
59 * `basic_managed_shared_memory`, this is probably not a good idea. That is none of our concern per se,
60 * as it does not change anything of what we do in a `*this`, but the assumed user Lend_tracker_pool
61 * probably doesn't want to use-up the entire pool-size of RAM, even when only a few pages of it
62 * are actually used. So we explicitly call our supposed containing-guy, in comments here, a
63 * `Sparse_managed_shm`.
64 *
65 * ### Rationale ###
66 * Why write it as a boost.ipc memory-algorithm specifically, particularly since it cannot be used for general
67 * data but only N-byte objects (not to mention its, as of this writing, rigid size properties)? Answer:
68 * it is convenient to then create a boost.ipc/extension `Sparse_managed_shm` -- with Use_count_registry as a
69 * key template parameter thereof -- at which point stuff slots-into the appropriate vaddr location with great
70 * ease of coding. The algorithm of finding an N-byte slot efficiently would have been what it is here; and it
71 * very much resembles a (narrow-use) memory-allocator; and we intended to use it in Lend_tracker_pool in SHM; so it
72 * is quite a natural fit.
73 *
74 * ### How to use ###
75 * Make a `Sparse_managed_shm` M in the normal way but with Use_count_registry as a template parameter.
76 * Probably you'll want to use `null_index` as a template parameter as well; though maybe not (not our business).
77 * Size the segment `"decltype(M)::Segment_manager::get_min_size() + Use_count_registry::base_offset()"`
78 * (or larger, but there's no point).
79 *
80 * We assume you'll want a single `Metadata` struct which is the only thing a `*this` will allocate() other
81 * than the 1-sized slots -- but only one, and it must be allocated up-front. Therefore, upon creating the pool,
82 * first-thing:
83 * - `const auto mdt = M->allocate(sizeof(Metadata)); construct_at(mdt, ...);`
84 * - If you don't need any such `Metadata`, use an empty `struct Metadata`. You must do that one allocation,
85 * even if it is ~0-sized.
86 *
87 * You can now use `*mdt` for whatever. Subsequently, you can (must) issue
88 * `const auto p = M->allocate(N); ...; M->deallocate(p);` at will. If storing things such as
89 * `atomic<uint32_t>` within these N-byte slots, remember to use:
90 * - `construct_at<T>(p, ...); // ctor args in ...; T is the particular N-byte-sized type.`
91 * - following `p = M->allocate(sizeof(T))`.
92 * - `static_cast<T*>(p)->~T()`
93 * - preceding `M->deallocate(p)`.
94 *
95 * Note: or if you chose to use non-`null_index` index, then you can do all that sweet built-in
96 * `M->construct<T>(...)(...)` stuff (and so on). That said, with `null_index`, one might guess they can
97 * still use `M->construct<T>(anonymous_instance)(...)` -- its being anonymous and all -- but no; that
98 * does not compile; apparently even anonymous-instance things still require a real index.
99 * Still, with `flow::util::construct_at()` utility it's close to the same level of syntactic sugar anyway.
100 * (In C++20 it is available as `std::construct_at()`, but as of this writing we are on C++17.)
101 *
102 * ### Resource use: RAM ###
103 * In the worst case a `*this` uses (as of this writing; see constants in body which can be adjusted):
104 * - 4 Mi (#S_USE_COUNTS_CAPACITY x N) bytes for the data
105 * (about `USE_COUNTS_CAPACITY` use-count slots and 1 metadata);
106 * - 1/8 Mi bytes for the bitmap and change (mandatory).
107 *
108 * However the former item starts at only 128 Ki (#S_USE_COUNTS_CAPACITY_QUANTUM_SZ times N) bytes RAM used.
109 * Another 128 Ki is added once (and if) that ever becomes insufficient; then another; and so on until all 4 Mi are
110 * taken. After that allocate() fails (returns null; so `M->allocate()` that wraps it throws `bad_alloc`).
111 *
112 * ### Resource use: processor cycles ###
113 * The algorithm and assumptions made (also in Lend_tracker_pool) are optimized for speed. As of this writing
114 * benchmark results are TBD however. There is no locking (see below). There is some trade-off to be had
115 * between the way the algorithm insists on using all of a *quantum* (128 Ki bytes as of this writing) before
116 * expanding to use another such quantum. It could be modified to expand earlier if, e.g., searches seem to be
117 * scanning too many slots during allocate() -- more aggressive (earlier) RAM use to increase speed. Such
118 * adjustments would be downstream of benchmarking in real-world situations. As of this writing there's decent
119 * reason to believe the way the bitmapping/scanning is set up allows for even worst-case searches to be quick.
120 *
121 * @todo Benchmark Use_count_registry, especially in the field, and adjust the allocate() algorithm accordingly,
122 * especially as concerns aggressiveness of expanding the active search field (for speed) at the expense of
123 * higher (earlier) RAM use. If in reality the number of live objects, e.g., rarely goes above 1,000, then there
124 * is no need to worry about it, as searches will be quick, *and* RAM use will be low.
125 *
126 * ### Limitations ###
127 * As noted at the top, this is not a memory-algorithm usable for storing generally-sized objects, as it supports
128 * exactly one such object (*metadata*) and beyond that only N-byte objects, where N is a constant (as of this writing:
129 * it is 4).
130 *
131 * Use_count_registry is, together with Lend_tracker_pool, based on the assumption that only one thread shall
132 * ever do allocate() and deallocate() on a given `*this`. Therefore it mandates the null "mutex" family
133 * inside any containing `Sparse_managed_shm`.
134 *
135 * For simplicity and resulting speed there is a rigid limit to how many N-byte use-counts can be stored in a
136 * `*this`: #S_USE_COUNTS_CAPACITY (as of this writing 1 Mi a/k/a ~1 million) slots. That is, it can represent
137 * 1 million live outer a/k/a first-class a/k/a `Lend_tracker_pool`-tracked a/k/a arena-`construct<>()`ed objects
138 * per admin-mode `*this`, a/k/a per (constructing) thread.
139 * - This should be plenty. (Side note: the first-`allocate()`-reserved metadata header will reduce the
140 * size available for use-counts but probably only a little. So we ignore this in these discussions.)
141 * - However if it is not plenty, we could increase #S_USE_COUNTS_CAPACITY. This would increase the
142 * bitmap size-in-bytes which is by definition 1/8 latter number -- and is like `S_USE_COUNTS_CAPACITY x N`
143 * paged-in only according to the quanta in use.
144 * - And/or we could be smarter/more dynamic about the bitmap itself also at the expense of code simplicity
145 * (and, possibly, speed).
146 *
147 * We never give RAM "back" (make it un-resident), outside of deleting the backing SHM-pool.
148 *
149 * ### The algorithm ###
150 * The above probably gives decent hints as to how it works (bitmap with 1 bit per N-byte/use-count slot;
151 * a "playing field" divided into smaller *quanta*, with more joining the field
152 * if needed). The rest can likely be gleaned by just reading the code, starting with the API and then the
153 * `private: // Data.` area.
154 */
156 private boost::noncopyable
157{
158public:
159 // Types.
160
161 /**
162 * This indicates that when using this as the memory-algorithm, we shall use no locking; this is a key
163 * assumption of our narrow use case (see class doc header). (In reality as of this writing the
164 * `Sparse_managed_shm` that will instantiate us will also use `null_index` -- there will be no
165 * lookup of objects by name, etc. -- so actually setting this has no effect; but if that were to change,
166 * then this would matter.)
167 */
168 using Mutex_family = ::ipc::bipc::null_mutex_family;
169
170 /// boost.ipc expects this stylization of #Mutex_family.
172
173 /**
174 * This alias is required for use in a `basic_managed_shared_memory`; `offset_ptr` is a solid default choice --
175 * though in our use case we do not believe this is actually used by anything externally (definitely not
176 * internally). (Our use in `Sparse_managed_shm` may or may not require this but can't hurt.)
177 */
178 using Void_pointer = ::ipc::bipc::offset_ptr<void>;
179
180 /// boost.ipc expects this stylization of #Void_pointer.
182
183 /// The usual `difference_type`.
184 using difference_type = ptrdiff_t;
185
186 /// The usual `size_type`.
188
189 /**
190 * Possibly required for things to build, but we do not supply an `allocate_many()`, so we set it to a dummy value.
191 * (Our use in `Sparse_managed_shm` may or may not require this but can't hurt.)
192 */
194
195 /// Short-hand for arena_lend::stat::Obj_db_aux_pool_stats;
197
198 // Constants.
199
200 /**
201 * This important constant defines the mandatory size of each allocation -- that is argument to allocate() --
202 * after the initial (metadata) allocation. So if it is 1, then you'll be storing, e.g., `atomic<uint8_t>`s or
203 * `uint8_t`s here; if it is 4 then, e.g., `atomic<uint32_t>`s or `uint32_t`s.
204 *
205 * ### Trade-offs ###
206 * The higher this is, the higher the (presumably) max use-count value you can store. That is good.
207 * On the other hand, the higher this is, the more RAM is used (at minimum and at maximum, both) to store
208 * the use-counts. The bitmap area's size does not depend on this; however the main/largest area
209 * can be sized up to `S_ALLOC_SZ * S_USE_COUNTS_CAPACITY` bytes; and is at minimum
210 * sized `S_ALLOC_SZ * S_USE_COUNTS_CAPACITY_QUANTUM_SZ`.
211 */
212 static constexpr size_t S_ALLOC_SZ = 4;
213
214 /**
215 * Compile-time knob: how many (N-byte) use-counts one `*this` can allocate.
216 * get_free_memory() is counted against this value times #S_ALLOC_SZ (a/k/a N) (hence begins equal to this times N,
217 * before first allocate()).
218 *
219 * The first `allocate(n)` is a special allocation, always at the top of this space, for the `*this` user(s)
220 * to hold a single metadata area; so in reality the use-counts capacity in bytes will be #S_USE_COUNTS_CAPACITY times
221 * N, minus `n` (rounded-up to a certain small multiple). Nevertheless it'll be close to this value all-in-all.
222 */
223 static constexpr size_t S_USE_COUNTS_CAPACITY = 1024 * 1024;
224
225 /// The most stringent alignment. E.g., in x86-64 this is 16, we've observed.
226 static constexpr size_t S_ALIGN_SZ = flow::util::max_align_sz();
227
228 /// boost.ipc expects this stylization of #S_ALIGN_SZ.
229 static constexpr size_t Alignment = S_ALIGN_SZ;
230
231 /**
232 * boost.ipc `segment_manager` requires this of a memory-algorithm: per-allocation book-keeping overhead in bytes.
233 * We keep all book-keeping externally (the use-count bitmap), so there is no such overhead.
234 *
235 * (In any case, as of this writing, in our use-case this should not be mentioned by anything at compile-time;
236 * but the member is required for the class to be instantiable at all, for some clang versions at least, while
237 * at least some compilers do not complain.)
238 */
239 static constexpr size_t PayloadPerAllocation = 0;
240
241 /**
242 * Default page size. We generally try to make larger vaddr areas sized as multiples of this and aligned
243 * on this versus the start of the SHM-pool in which we reside. That is not life-or-death whatsoever, but
244 * we figure, if there will be stats about things like resident pages, we might as well cleanly operate on entire
245 * such pages and get nice clean counts perhaps.
246 *
247 * Public for informational (not algorithmic) purposes: reporting, logging, diagnostics.
248 */
249 static constexpr size_t S_PAGE_SZ = 4 * 1024;
250
251 /**
252 * The size of each *quantum* into a whole number of which the data area (sized #S_USE_COUNTS_CAPACITY N-byte values)
253 * is divided. At any given time, the data area we've actually touched (thus paged-in: meaning actual RAM is
254 * being used as opposed to mere vaddr space) consists of 1+ quanta, starting at the start of the
255 * data area. As of this writing the algorithm is: start with 1 quantum (hence this much of the actual data
256 * area = real RAM); allocate from there, until no use-count slots are free to use; extend to 2 quanta;
257 * repeat; ...; until all `S_USE_COUNTS_CAPACITY` slots have been used (no more quanta available).
258 * As of this writing we do not un-page any pages or decrement the # of quanta.
259 *
260 * Public for informational (not algorithmic) purposes: reporting, logging, diagnostics.
261 */
262 static constexpr size_t S_USE_COUNTS_CAPACITY_QUANTUM_SZ = 32 * 1024; // 32Ki use-counts (each N bytes) per quantum.
263
264 // Constructors/destructor.
265
266 /**
267 * Constructs a `*this`, assumed to be in a SHM-pool (segment) at the time the `Sparse_managed_shm` is
268 * being created in SHM. So the creating-side would use this ctor, whereas any opening-side(s) would
269 * already work on a existing `*this` (already cted).
270 *
271 * @param sz
272 * Total size of SHM-segment.
273 * @param extra_hdr_sz
274 * Size required for us to not touch, between the end of our header (`*this`) and the start of the
275 * area from which allocate() shall draw. (It is used by `segment_manager` to stores its stuff --
276 * though in our case as of this writing we have `null_index` and `null_mutex_family`, so this is probably
277 * zero. We don't assume that though.)
278 */
279 Use_count_registry(size_t sz, size_t extra_hdr_sz);
280
281 // Methods.
282
283 /**
284 * Size we require at max capacity (all #S_USE_COUNTS_CAPACITY times #S_ALLOC_SZ bytes used) for the containing
285 * SHM-pool, including us and the `extra_hdr_sz` for the segment manager, excluding base_offset().
286 *
287 * @param extra_hdr_sz
288 * See above.
289 * @return See above.
290 */
291 static constexpr size_t get_min_size(size_t extra_hdr_sz);
292
293 /**
294 * The number of bytes between the start of the SHM-pool (segment) storing us and us (`*this`). It is
295 * a #Lend_tracker_shm fact (its `S_SEGMENT_OFFSET`). Use_count_registry itself does use it for the layout math
296 * in get_min_size() (and in stats) at least, so we declared it here.
297 *
298 * ### Rationale ###
299 * Internally we prefer to keep the main data area (with #S_USE_COUNTS_CAPACITY times #S_ALLOC_SZ bytes available
300 * to allocate()) aligned to the page size (and a multiple of page size), and certain subdivisions therein
301 * are also similarly aligned to/multiples of page size (4Ki bytes as of this writing/Linux/x86-64 at least),
302 * relative to the start of the containing SHM-pool. Whether that's a worthwhile endeavour or not is not
303 * to be litigated here; but as of this writing we undertake it.
304 *
305 * So in order to undertake it successfully, knowing where `this` is and `extra_hdr_sz` passed to ctor (which
306 * specifies size of area *after* `*this` but before our data area sized #S_USE_COUNTS_CAPACITY) is not quite
307 * enough: the pool places `*this` not at byte 0 of itself but after a little header of its own, and that slides
308 * the whole thing over by that much. (Trivia: that header is where boost.ipc's SHM-create-or-open algorithm
309 * keeps literally 32 bits of state -- whether the SHM-creating process has gotten as far as placing the
310 * segment-manager/memory-algorithm (the latter = us) into the just-created pool; SHM-opening processes spin on
311 * it before touching anything else -- padded to the segment-manager's alignment: #S_ALIGN_SZ as of this writing.)
312 *
313 * @return See above.
314 */
315 static constexpr size_t base_offset();
316
317 /**
318 * Allocates space out of #S_USE_COUNTS_CAPACITY times #S_ALLOC_SZ bytes available originally, and returns the
319 * address to the start of the following: the metadata header area (first call since pool creation) or a new
320 * use-count (subsequently).
321 *
322 * As explained in our class doc header, we are not a general memory-algorithm but rather for a very narrow use.
323 * Specifically:
324 * - The 1st allocate() call shall always return address at the start of the data area (sized
325 * up to #S_USE_COUNTS_CAPACITY times #S_ALLOC_SZ). `sz` bytes are available as the *metadata area* for whatever
326 * stuff you want to store in addition to the bulk use-counts (maybe stats or make-shift communications between
327 * pool users).
328 * - `sz == 0` is allowed (meaning no metadata area needed). You must still make the `allocate(0)` initial
329 * call in this case.
330 * - If `sz > S_USE_COUNTS_CAPACITY_QUANTUM_SZ * S_ALLOC_SZ` (1 quantum's worth of the data area), behavior is
331 * undefined.
332 * - All allocate() calls after the 1st are *normal* allocations.
333 * - `sz != S_ALLOC_SZ`, in the spirit of forgiveness, yields null and allocates no space. Otherwise:
334 * - `sz == S_ALLOC_SZ`: N (a/k/a #S_ALLOC_SZ) bytes are allocated for a new use-count and its address returned.
335 * - This address is *not* necessarily aligned to the most stringent alignment #S_ALIGN_SZ; but it is
336 * aligned to #S_ALLOC_SZ. (boost.ipc non-reference docs suggest allocate() must return a stringently
337 * aligned address; but formally that is not the case, in that boost.ipc won't barf if this isn't heeded.
338 * They probably mean that for use as a memory-algorithm for general allocation, `malloc()`-like
339 * functions like this shall return stringently aligned addresses; as otherwise processor exceptions and other
340 * errors can result when placing larger-than-N-byte structures at such addresses.)
341 *
342 * Last but not least, to restate the doc header: allocate(), deallocate() (as well as all `stat*()` methods)
343 * shall all be called on one `*this`, never concurrently.
344 *
345 * ### Errors ###
346 * Per standard expectations: if one runs out of #S_USE_COUNTS_CAPACITY times N bytes (a/k/a get_free_memory() is
347 * insufficient to yield a successful *normal* allocation), returns null.
348 *
349 * @param sz
350 * See above.
351 * @return An address or null. If used properly (no `sz != N` after allocation number one),
352 * null means we've run out of pool space.
353 */
354 void* allocate(size_t sz);
355
356 /**
357 * Reverses an earlier allocate() call. Caveats:
358 * - `!addr` => no-op.
359 * - `addr` equals that returned by allocate() number one (the metadata one) => no-op. That area cannot be
360 * returned.
361 * - `addr` neither null nor pointing to currently allocated area => undefined behavior.
362 *
363 * @param addr
364 * See above.
365 */
366 void deallocate(void* addr) noexcept;
367
368 /**
369 * Returns the value passed to ctor, when the pool was created.
370 * @return See above.
371 */
372 size_t get_size() const;
373
374 /**
375 * No-op: as of this writing a `*this` will allocate up to #S_USE_COUNTS_CAPACITY times #S_ALLOC_SZ, and requires the
376 * pool to be sufficiently sized (get_min_size()) from the start; and will not use additional space beyond that.
377 *
378 * Internally, there is a fixed-size bitmap that we do not allow to grow somehow; hence additional space does
379 * not help. That said: grow() is generally not for SHM-pools anyway but for other managed-vaddr-area types
380 * (files, who knows).
381 *
382 * @param extra_sz
383 * It is ignored.
384 */
385 void grow(size_t extra_sz);
386
387 /**
388 * How many `allocate(N)` calls, times #S_ALLOC_SZ, would succeed now without `deallocate()` in-between,
389 * until one returned null. If allocate() has not yet been called, then assume `allocate(0)` has been
390 * called (0-sized metadata area); the preceding sentence is then accurate.
391 *
392 * @return See above. Starts at #S_USE_COUNTS_CAPACITY times #S_ALLOC_SZ.
393 */
394 size_t get_free_memory() const;
395
396 /**
397 * Convenience function that returns the metadata area as a properly-typed pointer. (This is not a
398 * standard boost.ipc memory-algorithm API obviously.)
399 *
400 * Returns null if any of these is true:
401 * - allocate() has not been called.
402 * - The first `allocate(n)` call was such that `n != sizeof(Metadata)` (presumable type mismatch).
403 *
404 * @tparam Metadata
405 * Presumably the type of the metadata area you're using.
406 * @return See above.
407 */
408 template<typename Metadata>
409 Metadata* get_metadata();
410
411 /**
412 * Returns a non-negative number that is some compile-time constant K times
413 * the number of *quanta* currently in-use by `*this` for the at-most-`USE_COUNTS_CAPACITYxALLOC_SZ`-sized
414 * data area. In short: see stats_record() doc header for how to use this. If you want to know the actual
415 * meaning then read on (Details below).
416 *
417 * ### Details ###
418 * The data area holds up-to #S_USE_COUNTS_CAPACITY use-counts (minus some space for `Metadata`, whose size
419 * is established by the first allocate()). This is divided into
420 * M = `S_USE_COUNTS_CAPACITY / S_USE_COUNTS_CAPACITY_QUANTUM_SZ` quanta. At first only 1 quantum is used, as a
421 * memory-use-reduction strategy. Therefore stat_quanta_active() shall return K times 1. Once that area runs out
422 * of slots, a quantum is added; so stat_quanta_active() shall return K times 2. Once both areas run out of slots,
423 * a 3rd quantum is added... etc. Potentially stat_quanta_active() can return up-to K times M. (As of this writing
424 * the number cannot go down: only stay same or sometimes increase.)
425 *
426 * If called before the first allocate(), then the data area has not yet been touched; so we return zero.
427 *
428 * Therefore, if one saves stat_quanta_active() before allocate() (including the 1st one) and then
429 * passes this to stats_record(), then:
430 * -# stats_record() shall itself obtain stat_quanta_active() and compare to the passed-in (pre-`allocate()`)
431 * one. If they are equal, nothing has changed (see stat::Obj_db_aux_pool_stats); no-op. Otherwise:
432 * -# The preceding allocate() causes stat_quanta_active() to go up. Therefore stats_record()
433 * shall update `*target_stats` accordingly (again: see stat::Obj_db_aux_pool_stats).
434 *
435 * stats_record() therefore shall do work only when necessary; and that will occur up to at most M times
436 * per underlying data area's lifetime and only via allocate(). That is rare. The stat_quanta_active()
437 * call (pre-`allocate()` and then post-`allocate()` inside stats_record) itself is each a single memory
438 * load and is cheap.
439 *
440 * @see stats_record(): Call this post-`allocate()`. Call stat_quanta_active() pre-`allocate()`. Pass
441 * the result of the latter into the former as `prev_quanta_active`.
442 * @see stats_record_at_deletion(): Call this to register that the aux-pool that contains `*this` shall be
443 * removed.
444 *
445 * @return See above.
446 */
447 size_t stat_quanta_active() const;
448
449 /**
450 * Updates the various GAUGEs and HI_WMARKs in `*target_stats`, if and only if it is appropriate to do so
451 * given the assumption that `*target_stats` currently holds the values when
452 * `this->stat_quanta_active() == prev_quanta_active` was the case. For most up-to-date, yet still reasonably
453 * performant, results `prev_quanta_active` should be obtained no later than following the most recent
454 * allocate() or the creation of `*this`; and stats_record() called after the most recent allocate().
455 *
456 * @see stat::Obj_db_aux_pool_stats doc header and individual stat-members' doc headers; having a grasp on these
457 * is quite helpful in understanding on this whole setup.
458 *
459 * @warning Since the essential values in `*target_stats` are GAUGEs, it is imperative that one
460 * call stats_record_at_deletion() when about to delete the containing SHM-pool. Otherwise
461 * GAUGEs like Obj_db_aux_pool_stats::m_resident_sz will never go down -- not even once all arenas
462 * have shut down.
463 *
464 * @note You can save the return value and pass it in as `prev_quanta_active` next time you call this.
465 * However simply calling stat_quanta_active() and saving it as a stack variable ahead of each
466 * allocate() involves less long-term state for you and should be ~equally performant.
467 *
468 * ### Rationale ###
469 * Why this dance with `prev_quanta_active`? Why can't `*this` save it automatically? For that matter why
470 * not just pass a `target_stats` to allocate() and have it update everything automatically? Answer: As for
471 * the latter: that is certainly doable and would simplify the API (fewer methods), but allocate() has a
472 * standard signature imposed by boost.ipc; calling `Sparse_managed_shm::allocate()` et al shall
473 * invoke `this->allocate()` without passing it any stat-set or `prev_quanta_active`; `*this` is officially
474 * a memory-algorithm. Granted, since Lend_tracker_pool is the only actual user of Use_count_registry, it
475 * could simply bypass `Sparse_managed_shm` allocate-API when allocating and just do something like
476 * `m_pool.get_segment_manager()->get_memory_algorithm()->our_own_custom_allocate()`. Yet we've decided, for
477 * better or worse, to be a "fully" usable memory-algorithm impl (in quotes because it can still only allocate
478 * one N-byte thing and then only 4-byte things, among other limitations); we're sticking to it; it might
479 * pay off via reusability. As for why not just store the pre-`allocate()` value of stat_quanta_active()
480 * in allocate() -- and not need to expose stat_quanta_active() as public -- this is again doable, but it's
481 * a little dirty. Recall that, as a memory-algorithm, `*this`'s data sits inside SHM -- all of it.
482 * So, yes, we could add a `size_t m_prev_quanta_active` to Header... but then we're storing in-SHM something
483 * stats-related that's accessed quite rigidly only by an admin-mode Lend_tracker_pool and not by other ones +
484 * not in 2+ processes. It's not an awful crime but a bit of a layering violation. Something like that
485 * naturally belongs in Lend_tracker_pool, so we give it that task. Given these constraints, we've tried
486 * to make the rest of the API as simple as possible to use.
487 *
488 * @param prev_quanta_active
489 * Result of stat_quanta_active() when `*target_stats` was last accurate w/r/t `*this`.
490 * (Note that, as the induction base, before the initial `this->allocate()` it is correct that
491 * `*this`'s contribution to `*target_stats` be zero for all GAUGEs in stat::Obj_db_aux_pool_stats.)
492 * @param target_stats
493 * Stats to update (only if necessary).
494 * @return stat_quanta_active() at this time.
495 */
496 size_t stats_record(size_t prev_quanta_active, Obj_db_aux_pool_stats* target_stats);
497
498 /**
499 * Assuming one has followed the required algorithm to update `*target_stats` so far, this method
500 * (intended to be called ahead of aux-pool deletion) shall undo (subtract) `*this`'s contribution to
501 * `*target_stats` for all relevant GAUGEs.
502 *
503 * @param target_stats
504 * See stats_record().
505 * @return stat_quanta_active() at this time (and, presumably, the final value thereof).
506 */
508
509private:
510 // Constants, types.
511
512 /**
513 * In the bitmap mapping the data area of size #S_USE_COUNTS_CAPACITY, this defines a *word* of bits;
514 * a *word* being so chosen so that it is the largest type, such that there is a high-performance intrinsic
515 * available that takes this type that can scan for the first 0 bit.
516 */
517 using bit_word_t = uint64_t;
518
519 // S_PAGE_SZ and S_USE_COUNTS_CAPACITY_QUANTUM_SZ are public (see above).
520
521 static_assert((S_PAGE_SZ % S_ALIGN_SZ) == 0,
522 "Come on now... PAGE_SZ should be a multiple of the most stringent alignment (16 or w/e).");
523
524 static_assert((((S_USE_COUNTS_CAPACITY * S_ALLOC_SZ) % S_PAGE_SZ) == 0)
525 && (S_USE_COUNTS_CAPACITY != 0)
526 && ((S_USE_COUNTS_CAPACITY % 8) == 0),
527 "Please set USE_COUNTS_CAPACITYxALLOC_SZ to a positive multiple of PAGE_SZ; "
528 "and set USE_COUNTS_CAPACITY to be a multiple of 8 (bits/byte).");
529
530 /**
531 * The total size of the bitmap area which uses 1 bit per 1 use-count slot (each N bytes wide) of the data area
532 * which is #S_USE_COUNTS_CAPACITY times N bytes in size.
533 */
534 static constexpr size_t S_BITMAP_SZ = S_USE_COUNTS_CAPACITY / 8;
535 static_assert((S_BITMAP_SZ % sizeof(bit_word_t)) == 0,
536 "Please set USE_COUNTS_CAPACITY to a multiple of [8 (bits/byte) x bit_word_t (8)].");
537
539 "Please set USE_COUNTS_CAPACITY_QUANTUM_SZ to USE_COUNTS_CAPACITY divided by some integer.");
540 static_assert((S_USE_COUNTS_CAPACITY_QUANTUM_SZ != 0) && ((S_USE_COUNTS_CAPACITY_QUANTUM_SZ % 8) == 0),
541 "Please set USE_COUNTS_CAPACITY_QUANTUM_SZ to a positive multiple of 8 (bits/byte).");
542
543 /// The size of each bitmap section (in bytes) corresponding to 1 quantum of use-count slots (each N bytes wide).
545 // 4Ki (=1 page) bytes in bitmap section. --^
546 static_assert((S_BITMAP_QUANTUM_SZ % sizeof(bit_word_t)) == 0,
547 "Please set USE_COUNTS_CAPACITY_QUANTUM_SZ to a multiple of [8 (bits/byte) x bit_word_t (8)].");
548
549 /**
550 * The raw data of the bitmap: an array of *words*, each word representing [`sizeof(word)` x 8] use-count slots
551 * (each N bytes wide) of the data area. So as of this writing that's N 64-bit words each standing for 64 use-count
552 * slots. One intrinsic call can find an unused (0) bit in one word (element in this array); hence 64 potential
553 * slots.
554 */
555 using Bitmap_words = boost::array<bit_word_t, S_BITMAP_SZ / sizeof(bit_word_t)>;
556
557 /// This is the data in `*this`: the stuff preceding the `extra_hdr_sz`-byte gap and then the data area.
558 struct Header
559 {
560 // Constructors/destructor.
561
562 /**
563 * Ctor that leaves all scalars uninitialized, `optional`s empty, and #m_bitmap_words untouched (therefore
564 * non-resident but zero-filled per that property of in-SHM vaddr areas).
565 *
566 * ### Rationale ###
567 * Why ctor exists instead of just letting us be aggregate-initialized a-la `{ sz, extra_hdr_sz, ... }`:
568 * See #m_bitmap_words doc header. It'd be shorter and nicer if we could just use the aggregate-init but can't.
569 *
570 * Why ctor does not take the init values for various scalars, which are going to be immediately set anyway
571 * by daddy Use_count_registry: It'd be pointless boiler-plate for a single-use `struct` type.
572 */
573 Header();
574
575 // Data.
576
577 /// See get_size(). Essentially `const` after being assigned.
578 size_t m_pool_sz;
579
580 /// See ctor and/or get_min_size(). Essentially `const` after being assigned.
582
583 /**
584 * For speed, precomputed/memorized offset from `this` (a/k/a the start of the `Header`) where
585 * the data area -- up to #S_USE_COUNTS_CAPACITY times #S_ALLOC_SZ bytes long -- begins.
586 * Essentially `const` after being assigned.
587 */
589
590 /**
591 * How much of the data area is in use, in bytes; i.e., the count of 1-bits in #m_bitmap_words,
592 * times #S_ALLOC_SZ; a/k/a `S_USE_COUNTS_CAPACITY * S_ALLOC_SZ - get_free_memory()`.
593 */
595
596 /// The size passed to the first (metadata) allocate(); or null until then.
597 std::optional<size_t> m_meta_obj_sz;
598
599 /**
600 * At all times a positive multiple of `S_BITMAP_QUANTUM_SZ / sizeof(bit_word_t)`, and at most
601 * `S_USE_COUNTS_CAPACITY / sizeof(bit_word_t) / 8`, this equals the number of #m_bitmap_words elements that
602 * represent the currently active quanta. I.e., it is the number of bitmap words, starting from
603 * the `m_bitmap_words.begin()`, through which allocate() shall search (in the bitmap).
604 * Once it fails to find a single 0-bit in the entire area, this is incremented by
605 * `S_BITMAP_QUANTUM_SZ / sizeof(bit_word_t)` (number of words in 1 quantum).
606 * If it already equals `m_bitmap_words.size()` -- i.e., `S_USE_COUNTS_CAPACITY / sizeof(bit_word_t) / 8`
607 * (all the available quanta) -- then allocate() fails (no slots available).
608 */
610
611 /**
612 * In the range [0, m_bitmap_words_sz), this is the index into #m_bitmap_words where the next search
613 * (in allocate()) will begin. Simply, this is the same word that the last allocate() found the 0-bit in;
614 * unless that bit was the last bit of the words; in which case it is the next word; unless that word would
615 * equal #m_bitmap_words_sz; then it is 0.
616 *
617 * So it'll search the same word again if there are bits left at the time in it (yes, that means potentially
618 * searching through some bits we already have seen to be 1), or the next word (wrapping around the
619 * active quanta) if none left in that word.
620 *
621 * Note for context: deallocate() could occur before the next allocate(); it could open up more 0 bits in
622 * the current word; so none of this is an exact science. It is a heuristic algorithm meant to keep moving
623 * forward/around so as to hopefully find 0-bits quickly each time.
624 */
626
627 /**
628 * The bitmap, where 0 represents a free slot; 1 a taken slot. Starts all zeroes; first special `allocate()`
629 * sets a small (at least 1) number of words at the start to consist of only 1-bits -- corresponding to the
630 * metadata area albeit rounded up to whole words for simplicity. These are never freed, as that `allocate()`
631 * cannot be `deallocate()`d. The rest of the bits are for the remaining (actual) `[de]allocate()` calls.
632 *
633 * C++ technicalities mean that we have to take some special measures to avoid zero-filling it (which (1) is
634 * redundant, as the in-SHM bytes are initially pre-zeroed, and (2) would make them resident which we -- see
635 * Use_count_registry ctor -- specifically don't want, as we can save some decent RAM in this area by avoiding it).
636 * Hence the presence of Header ctor.
637 */
639 }; // struct Header
640
641 // Data.
642
643 /// See Header doc header.
645}; // class Use_count_registry
646
647// Template and constexpr implementations.
648
650{
652 /* Why aren't we a (still static constexpr) data member/constant? That would've been fine too. This matches
653 * the subtly related get_min_size() (which does need to be a function), so that seemed nice. */
654}
655
656constexpr size_t Use_count_registry::get_min_size(size_t extra_hdr_sz)
657{
658 using flow::util::round_to_multiple;
659
660 /* Using similar technique to what boost.ipc rbtree_best_fit uses here.
661 *
662 * Do note that, as of this writing, the true size used (with all allocate() space used-up to the byte)
663 * is an exact number; we cannot grow (or shrink) from there. So get_min_size() is not just the minimal
664 * size of the SHM-pool; it is also the *exact* size actually (at max) *used*: we will specifically
665 * set (see our ctor, where we set m_data_start_minus_this) the start of the NxUSE_COUNTS_CAPACITY-sized
666 * allocate()-used area to get_min_size(extra_hdr_size) minus NxUSE_COUNTS_CAPACITY. Empirically speaking, at least,
667 * as of this writing if one sets the SHM-pool size to `segment_manager::get_min_size() + base_offset()`,
668 * it is enough, and things work; and if one sets it to a byte less -- a boost.ipc internal assertion trips
669 * due to insufficient size.
670 *
671 * So first C++ shall (basically) put, like, a `struct { Header ...; segment_manager sized extra_hdr_sz; };`.
672 * Those should take up at worst a tight multiple of the strictest alignment which is ALIGN_SZ.
673 * Next is our data area, which is USE_COUNTS_CAPACITYxALLOC_SZ bytes, as ALLOC_SZ-wide slots. However for certain
674 * reasons (see comment elsewhere for rationale) we choose to start that area on the lowest possible
675 * multiple of PAGE_SZ. Hence however much we think is conservatively required for that fake "struct" --
676 * round *that* further up to PAGE_SZ... then the data area.
677 *
678 * ...Oh... but also... see doc header for base_offset(), then come back here. Long story short:
679 * `*this` actually shall be placed after a short additional header (ALIGN_SZ-multiple-sized like the other
680 * stuff), so include that in the calculation to leave the start of NxUSE_COUNTS_CAPACITY data area
681 * PAGE_SZ-aligned versus the start of the containing SHM-pool. */
682
683 static_assert((base_offset() % S_ALIGN_SZ) == 0, "boost.ipc is known to align this stringently.");
684 return round_to_multiple(base_offset()
685 + round_to_multiple(sizeof(Header), S_ALIGN_SZ)
686 + round_to_multiple(extra_hdr_sz, S_ALIGN_SZ),
687 S_PAGE_SZ)
689 /* The sum so far = actual desired (min/forever) pool size, starting from byte 0 of base_offset()-header
690 * and ending at last byte of our data-area. And it's sized in such a way, if we did the rounding math
691 * right, that data-area begins on a page's byte 0, relative to the pool start. A bunch of slack likely
692 * precedes this byte 0, due to the rounding-up of previous areas to the nearest PAGE_SZ.
693 *
694 * That's great. However, we aren't being asked to return *that* size, but the size relative to `this`
695 * (== &m_header), which sits after base_offset() bytes. Hence: */
696 - base_offset();
697 /* @todo Check an above thing empirically: see if data-area minus actual-pool-start mod PAGE_SZ, is zero.
698 * assert() or unit-test it somewhere. Just be careful to get "actual-pool-start" at the right/sufficiently
699 * high level (probably not in here), so that it is convincing without having to assume some of the same arithmetic
700 * we are assuming here w/r/t what area starts where versus what other address. */
701}
702
703template<typename Metadata>
705{
706 // Not allocate()d yet (possibly by other process), or allocate()d to apparently the wrong size: return null.
707 return (m_header.m_meta_obj_sz && (sizeof(Metadata) == *m_header.m_meta_obj_sz))
708 ? reinterpret_cast<Metadata*>(reinterpret_cast<uint8_t*>(this) + m_header.m_data_start_minus_this)
709 : nullptr;
710}
711
712} // namespace ipc::shm::arena_lend::detail
A boost.ipc-compliant memory-algorithm suitable for use in a bipc_ext::Sparse_managed_shm (which is a...
static constexpr size_t Alignment
boost.ipc expects this stylization of S_ALIGN_SZ.
static constexpr size_t get_min_size(size_t extra_hdr_sz)
Size we require at max capacity (all S_USE_COUNTS_CAPACITY times S_ALLOC_SZ bytes used) for the conta...
size_t get_free_memory() const
How many allocate(N) calls, times S_ALLOC_SZ, would succeed now without deallocate() in-between,...
void grow(size_t extra_sz)
No-op: as of this writing a *this will allocate up to S_USE_COUNTS_CAPACITY times S_ALLOC_SZ,...
static constexpr size_t base_offset()
The number of bytes between the start of the SHM-pool (segment) storing us and us (*this).
static constexpr size_t S_BITMAP_QUANTUM_SZ
The size of each bitmap section (in bytes) corresponding to 1 quantum of use-count slots (each N byte...
static constexpr size_t S_USE_COUNTS_CAPACITY_QUANTUM_SZ
The size of each quantum into a whole number of which the data area (sized S_USE_COUNTS_CAPACITY N-by...
static constexpr size_t S_ALLOC_SZ
This important constant defines the mandatory size of each allocation – that is argument to allocate(...
void deallocate(void *addr) noexcept
Reverses an earlier allocate() call.
::ipc::bipc::offset_ptr< void > Void_pointer
This alias is required for use in a basic_managed_shared_memory; offset_ptr is a solid default choice...
static constexpr size_t PayloadPerAllocation
boost.ipc segment_manager requires this of a memory-algorithm: per-allocation book-keeping overhead i...
Metadata * get_metadata()
Convenience function that returns the metadata area as a properly-typed pointer.
size_t stats_record_at_deletion(Obj_db_aux_pool_stats *target_stats)
Assuming one has followed the required algorithm to update *target_stats so far, this method (intende...
static constexpr size_t S_ALIGN_SZ
The most stringent alignment. E.g., in x86-64 this is 16, we've observed.
::ipc::bipc::null_mutex_family Mutex_family
This indicates that when using this as the memory-algorithm, we shall use no locking; this is a key a...
size_t stats_record(size_t prev_quanta_active, Obj_db_aux_pool_stats *target_stats)
Updates the various GAUGEs and HI_WMARKs in *target_stats, if and only if it is appropriate to do so ...
uint64_t bit_word_t
In the bitmap mapping the data area of size S_USE_COUNTS_CAPACITY, this defines a word of bits; a wor...
static constexpr size_t S_BITMAP_SZ
The total size of the bitmap area which uses 1 bit per 1 use-count slot (each N bytes wide) of the da...
static constexpr size_t S_PAGE_SZ
Default page size.
Mutex_family mutex_family
boost.ipc expects this stylization of Mutex_family.
boost::array< bit_word_t, S_BITMAP_SZ/sizeof(bit_word_t)> Bitmap_words
The raw data of the bitmap: an array of words, each word representing [sizeof(word) x 8] use-count sl...
void * allocate(size_t sz)
Allocates space out of S_USE_COUNTS_CAPACITY times S_ALLOC_SZ bytes available originally,...
ptrdiff_t difference_type
The usual difference_type.
size_t stat_quanta_active() const
Returns a non-negative number that is some compile-time constant K times the number of quanta current...
Use_count_registry(size_t sz, size_t extra_hdr_sz)
Constructs a *this, assumed to be in a SHM-pool (segment) at the time the Sparse_managed_shm is being...
static constexpr size_t S_USE_COUNTS_CAPACITY
Compile-time knob: how many (N-byte) use-counts one *this can allocate.
size_t get_size() const
Returns the value passed to ctor, when the pool was created.
int multiallocation_chain
Possibly required for things to build, but we do not supply an allocate_many(), so we set it to a dum...
Void_pointer void_pointer
boost.ipc expects this stylization of Void_pointer.
static constexpr size_t S_SEGMENT_OFFSET
Offset from pool-base at which Segment_manager is placed.
Segregated private stuff for ipc::shm::arena_lend.
This is the data in *this: the stuff preceding the extra_hdr_sz-byte gap and then the data area.
Header()
Ctor that leaves all scalars uninitialized, optionals empty, and m_bitmap_words untouched (therefore ...
size_t m_data_start_minus_this
For speed, precomputed/memorized offset from this (a/k/a the start of the Header) where the data area...
size_t m_extra_hdr_sz
See ctor and/or get_min_size(). Essentially const after being assigned.
size_t m_bitmap_words_next_search_idx
In the range [0, m_bitmap_words_sz), this is the index into m_bitmap_words where the next search (in ...
Bitmap_words m_bitmap_words
The bitmap, where 0 represents a free slot; 1 a taken slot.
std::optional< size_t > m_meta_obj_sz
The size passed to the first (metadata) allocate(); or null until then.
size_t m_pool_sz
See get_size(). Essentially const after being assigned.
size_t m_allocated_sz
How much of the data area is in use, in bytes; i.e., the count of 1-bits in m_bitmap_words,...
size_t m_bitmap_words_sz
At all times a positive multiple of S_BITMAP_QUANTUM_SZ / sizeof(bit_word_t), and at most S_USE_COUNT...
Stats about the RAM use of the arena-lending SHM-provider's auxiliary (non-user-data) SHM-pools which...