Purpose: Set the library's widths, sizes and optional modules for your target, and know which of them are derived and never yours to set. Scope: include/mmgr.h, deps/embedded_types/include/, CMakeLists.txt Author: dstroy0 (Douglas Quigg) dquig.nosp@m.g123.nosp@m.@gmai.nosp@m.l.co.nosp@m.m Date: 2026-08-30
Nothing here is set at runtime. Every value below is a preprocessor define with a default: you set only what you are changing. The last section is the exception: those are CMake options and never defines.
These come from embedded_types, not from MMgr, and carry its prefix.
| knob | default | what it changes |
|---|---|---|
| EMBED_WORD_BITS | derived from UINTPTR_MAX | the SWAR carrier and embed_word. 64, 32 or 16 |
| EMBED_INDEX_BITS | 32, or EMBED_WORD_BITS if narrower | the type of an offset into a region |
Setting EMBED_WORD_BITS narrower than the machine does not make anything faster: it makes the scanner answer for fewer bytes per load. It exists to let a wide host exercise a narrow machine's code paths. See Doing eight bytes at a time.
embed_types.h carries static asserts that police the combination; idx16 is the environment that exists to reach them.
No knob sets a size here. The size is in the declaration: ParsMemoriaeInternae(work, 4096) states the extent where the storage exists, and sizeof is asserted against the count it was handed so the two cannot disagree.
| knob | default | what it changes |
|---|---|---|
| MMGR_ALIGN_BYTES | 16 | alignment every pool declaration puts on its storage |
| MMGR_CARCER_ALIGN | sizeof(embed_word) | derived. The alignment every cell is handed out at |
| MMGR_EXTRAM_ATTR | empty | placement an external pool carries. The port supplies it |
MMGR_EXTRAM_ATTR is named here and filled by the port, because which section a part puts external memory in is the part's business. A build that enables external memory and leaves this empty gets pools placed wherever the linker puts an ordinary one, and nothing diagnoses that.
These are the numbers you change after measuring. Do not guess them. See Your first prison site for reading the high-water marks.
| knob | default | what it changes |
|---|---|---|
| MMGR_CARCER_OWNERS | 128 | the most owners a program's owner table holds |
| MMGR_MOVABLE_SETTLE_PERCENT | 25 | the fragmentation at which a cellblock's mover sorts |
| MMGR_MOVABLE_READ_MS | 100 | the milliseconds between the mover's readings |
| MMGR_MOVABLE_QUIET_READINGS | 3 | the readings in a row of low occupancy before it locks a turnstile |
| MMGR_MOVABLE_BACKOFF_MOST_MS | 25600 | the longest it waits after a sort it backed off from |
A cellblock is shared by every context that reaches it, with no switch and no lock. Every boundary, every cell's claim and every count is an atomic word from embedded_types, moved in one step each, and no context waits for another. The same build serves a host thread, a part's task and a GPU thread.
An owner's own counts, its bytes in use and its peak, are kept in the CellOwner record the program's owner table holds. MMGR_OWNERS lays the table once, as data, from the program's list of owners, and defines each owner's const pointer into it, which the owner's module names on every take, give and growth. A list longer than MMGR_CARCER_OWNERS fails the build. See Who owns what.
| knob | default | what it changes |
|---|---|---|
| MMGR_DEBUG_CHECKS | 0 | compiles in the checks, and selects the trapping assert |
| MMGR_ASSERT(cond, msg) | a type-checked no-op | what a broken precondition does |
The default MMGR_ASSERT keeps its expression type-checked with sizeof and then discards it: it cannot rot and costs nothing.
MMGR_DEBUG_CHECKS=1 is all it takes to trap: test/support/mmgr_host_traps.h then supplies MMGR_ASSERT, as a report to stderr naming the expectation, the file and the line, followed by abort(). That report is the checks environment entire. Define MMGR_ASSERT yourself before including the header and neither form is used, the shape a target with no stderr and no abort() wants. See How failure is reported.
| knob | default | what it changes |
|---|---|---|
| MMGR_ENABLE_DMA | 0 | compiles memoriam_praetereo/, included from mmgr.h |
| MMGR_ENABLE_EXTRAM | 0 | compiles memoria_externa/, included from mmgr.h |
With these off, the modules are absent entirely, with no stubs. Their test suites are skipped with a CMake status message instead of being silently dropped.
Read only where MMGR_ENABLE_DMA is on. None of these has a value that is right for every part, and a build answers them. From CMake, pass them as one list in MMGR_PRAET_KNOBS, which reaches the library and every suite over it:
| knob | default when unset | what it changes |
|---|---|---|
| PRAET_CHANNELS | 8 | logical channels one context carries |
| PRAET_SETTLE_MICROS | 0 | microseconds the engine takes to come up after an attach |
| PRAET_KEEPALIVE_MICROS | 1000 | microseconds a moving channel may go unkicked before it stalls |
| PRAET_RECOVERY | 0 | whether a stalled transfer can be backed out or scrubbed |
| PRAET_CLOCK_HZ | 1000000 | ticks per second of the clock the deadlines are scaled from |
| PRAET_CLOCK_SOURCE | derived from the architecture | PRAET_CLOCK_CALLER or PRAET_CLOCK_OWN |
| PRAET_CLOCK_CORE | PRAET_PLATFORM_CLOCK_CORE | the core a pinned timer runs on, read only on PRAET_CLOCK_OWN |
Each unset knob takes its default and raises a warning naming itself. After every knob has been read, praet_iudex.h stops the build once if any of them was unset. Define MMGR_ACCEPT_DEFAULTS to build on the defaults and keep the warnings as the record.
| knob | default | what it changes |
|---|---|---|
| MMGR_STR_MAX | see mmgr_string_shim.h | the read cap the shim's str* replacements use |
| MMGR_RING_LOCULI | 8 | loculi this build reserves |
| MMGR_RING_LOCULI_MAX | EMBED_WORD_BITS | loculi a mask can address, one bit per loculus in a machine word |
| MMGR_RING_WORDS | 40 | size of the ring storage a caller declares, in size_t units |
| MMGR_SIEVE_ROWS | 1 | needle offsets the search sieve tests per candidate word |
| MMGR_FIND_CHAIN_MAX | SIZE_MAX | longest haystack a one or two byte needle is settled by mask chain |
MMGR_RING_LOCULI_MAX is derived, not a knob, and it is EMBED_WORD_BITS instead of a fixed 32: the free and busy masks are one embed_word each, one bit per loculus (src/memoria_anularis/memoria_anularis.c:308-309), and the ceiling moves with the word width. A build declaring more loculi than that fails the static assert in src/memoria_anularis/memoria_anularis.h:84.
The byte-frequency profile is a CMake option taking a value in place of a set of defines: -DMMGR_ANCORAE_FORMA=english, and likewise uri, inet, route or generic. One profile is one translation unit, src/impensa_ancorae_acus/CMakeLists.txt:7-15 selects the single source that carries the table, and the four not chosen are not in the image at all. It defaults to generic, and picking the wrong one costs speed and never correctness: the search still finds what is there. See Anchor cost: where a search starts.
MMGR_FIND_CHAIN_MAX defaults to no limit, which folds its test away: read_cap <= SIZE_MAX holds for every size_t. A default build emits no comparison. A one or two byte needle is settled by a mask chain, one broadcast per needle byte, every start position in the word decided at once, nothing to verify, in place of building the sieve, which exists to find a rare byte in a long needle and prove the rest once. Measured with a two byte needle, cycles for the whole call:
| n | 8 | 64 | 2048 |
|---|---|---|---|
| Xtensa chain | 124 | 489 | 13391 |
| Xtensa sieve | 187 | 607 | 15494 |
| RISC-V chain | 124 | 488 | 13393 |
| RISC-V sieve | 219 | 680 | 17059 |
The chain wins at every length on both parts: nothing needs setting. The knob is kept because a measurement proves nothing about an unmeasured length; zero sends every needle through the sieve.
Case folding always goes through the sieve: the chain compares raw bytes.
These come from embedded_types as well.
| knob | default | what it changes |
|---|---|---|
| EMBED_BIG_ENDIAN | derived from __BYTE_ORDER__ | the host's byte order |
| EMBED_FAST_UNALIGNED_LOAD | derived from __ARM_FEATURE_UNALIGNED etc | whether a word load takes any address |
| EMBED_INLINE | static inline, plus always_inline where available | how hot entries are inlined |
| EMBED_FLATTEN | __attribute__((flatten)) where available, else nothing | a caller's lever to inline an entry |
EMBED_FLATTEN is for a caller and never for the library. Put it on the one hot function that reaches an entry and the compiler inlines the entry into it, which the inliner otherwise declines to do on size even under link-time optimization. Measured on an ESP32-S3, cellul.len over eight bytes is 112 cycles called and 80 inlined, a third of the work at that length. What it costs has the table and the caveats: it needs LTO, it costs the walk's code at every site that takes it, and a long scan amortizes the call and will not notice.
EMBED_FAST_UNALIGNED_LOAD is not whether an unaligned load compiles: every target accepts one through a type carrying EMBED_RAW, which is EMBED_ALIGN(1) and EMBED_ALIAS. It is whether the hardware does it in one instruction, or the compiler assembles the word out of byte loads and shifts. Measured on a single such load: ARMv7-M emits one ldr, Xtensa twelve instructions, RISC-V eleven. A walk that needs the word at an offset of one takes the load where it is one instruction and derives it from the word already in hand where it is a dozen.
It is not a statement about the family either. Cortex-M0 is ARMv6-M, has no unaligned access, and the macro reports it slow: the compiler answers through __ARM_FEATURE_UNALIGNED, which -mno-unaligned-access also turns off on a part that has it.
Every compiler conditional lives in embed_compiler_directives.h by policy: supporting a new toolchain is one file to read. See Compilers, attributes and fallbacks.
These are build options and never preprocessor defines.
| option | default | what it does |
|---|---|---|
| MMGR_BUILD_TESTS | ON | build the unit and environment suites |
| MMGR_BUILD_BENCH | OFF | build the benchmarks |
| MMGR_WERROR | OFF | treat warnings as errors |
| MMGR_LTO | ON | link-time optimization, where the toolchain supports it |
One more thing the build does that is worth knowing: MinSizeRel normally means -Os, and -Os declines to inline the small branch-free entries that are this library, and the calls it saves space on are the calls that cost the most. The top-level CMakeLists.txt rewrites -Os to -O2 for that configuration. An embedded build reaching for MinSizeRel wants small, not slow-and-small.