MMgr v1.0.0
Zero-heap memory manager in C11. One borrowed buffer, carved by asserted offset.
Loading...
Searching...
No Matches
Memoria anularis: the lock-free edge

Single-producer, single-consumer. A byte ring, a segment view over the same bytes, and loculus keepouts.

When to reach for it

  • One context produces and exactly one consumes.
  • An interrupt hands bytes to a task, or a task hands buffers to a driver.
  • You want that handover without a lock.

What it composes with

It moves what Locus carcerum: the region handed out. The segment view passes whole buffers by index, and nothing is copied across the boundary.

Three things over the same bytes

A byte ring. available, vacant, put, read, read_byte, peek, consume. Capacity must be a power of two: the wrap is a mask instead of a modulo.

A segment view. seg_next, seg_publish, seg_front, seg_release, seg_at, seg_inflight. The producer fills a segment and publishes its index; the consumer reads that index and releases it when done. The bytes never move.

Loculi. loculus_ready, loculus_next, loculus_hold, loculus_keepout, loculus_drop, loculus_mark. A numbered hold that records a region to keep out of, for a reader walking bytes in place. Their free and pinned masks are one machine word each, and so MMGR_RING_LOCULI_MAX is EMBED_WORD_BITS. MMGR_RING_LOCULI is the build knob under that ceiling, and a static assert names it if it is set higher; a build with no use for them sets it to 0 and gets the keepout storage back.

Worked example

The caller declares the ring and supplies the bytes. Nothing of the state is reachable from outside:

ParsMemoriaeInternae(ring_bytes, 4096);
MemoriaAnularis(ring, ring_bytes);
EMBED_CALL(anularis.init, AnularisCfg, .ring = &ring, .buf = mmgr_pars_storage_ring_bytes,
.capacity = ring_bytes_bytes, .segment_count = 8u);
#define MemoriaAnularis(name_, pool_)
Declares a ring's storage and claims the pool it is laid over.
#define ParsMemoriaeInternae(name_, bytes_)
Declares a pool of bytes in internal memory.
Definition mmgr.h:188
Arguments for every mmgr_anular_ entry, each reading only the members it needs.
mmgr_ring *const ring

MemoriaAnularis declares the ring's storage and claims the pool. The claim stops those same bytes being dressed a second time, as a cellblock, or as another ring, which would put two sets of records over one region with nothing at run time to report it.

.segment_bytes gives the segments their size where the capacity divided by the count is not the size wanted. The capacity can then be any size that holds them, since a segment's place is its index times its size and the segment counters wrap by the count, which stays a power of two. Such a ring has no byte stream: vacant reads 0, put is refused and read takes nothing.

Filling and draining:

if (!EMBED_CALL(anularis.put, AnularisCfg, .ring = &ring, .src = msg, .bytes = n)) {
/* the whole span did not fit; nothing was written */
}
const size_t got = EMBED_CALL(anularis.read, AnularisCfg, .ring = &ring, .dst = out, .bytes = n);

Passing whole segments by index instead, and so nothing is copied across the boundary:

size_t seg = 0;
if (EMBED_CALL(anularis.seg_next, AnularisCfg, .ring = &ring, .out_index = &seg)) {
fill(EMBED_CALL(anularis.seg_at, AnularisCfg, .ring = &ring, .index = seg));
EMBED_CALL(anularis.seg_publish, AnularisCfg, .ring = &ring);
}
if (EMBED_CALL(anularis.seg_front, AnularisCfg, .ring = &ring, .out_index = &seg)) {
use(EMBED_CALL(anularis.seg_at, AnularisCfg, .ring = &ring, .index = seg));
EMBED_CALL(anularis.seg_release, AnularisCfg, .ring = &ring);
}

seg_next and seg_front answer whether there is one and write the index through .out_index, and therefore a caller never needs a sentinel index to mean "none".

Gotchas

SPSC only. Two producers on one ring is a broken program and not a slow one. There is no check and no assert: exactly one producer advances head and exactly one consumer advances tail. For that reason alone acquire and release ordering is enough and no entry on those two needs a read-modify-write.

put is all or nothing. It checks the whole span against vacant first. A partial write never happens and a half span is never visible. It answers EMBED_FALSE instead of writing what fits.

peek copies what you ask for regardless of whether it arrived. Read available first. A count above the ring's capacity is clamped there, since one lap is all two runs can express.

Nothing is ever zeroed. A loculus keeps its bytes after a drop. A restream can run again over the same region. Bytes read are not scrubbed behind the cursor either.

Capacity must be a power of two. The index wrap is a mask. nsegs likewise, and at most cap.

The buffer is aligned before the ring gets it. The ring does not check and has no way to.

loculus_next is a SWAR popcount fold and not a builtin. Same reasoning as the rest of the library, see Doing eight bytes at a time.

Self-contained. It reaches nothing outside config: the span mover, the bit index and the span type it records are all defined in its own .c, which is also where <stdatomic.h> stays.

Reference

Generated reference · Who owns what for what a pointer's lifetime is


Memoria externa: DRAM or PSRAM

Decides where a buffer should live when there is more than one kind of memory.

Note
Compiled only when MMGR_ENABLE_EXTRAM is set. It defaults off, and its test suite is skipped loudly and not silently.

When to reach for it

A part with both internal SRAM and external PSRAM, where the trade is real: internal is fast and scarce, external is large and slower, and some of it cannot be reached by DMA.

What it does

Every entry takes one argument pack, as the rest of the library does, and so the six figures are named and not positional:

const mmgr_place where = EMBED_CALL(exter.place, ExternaCfg, .size = want, .dma_required = needs_dma,
.free_dram = dram_left, .free_psram = psram_left,
.psram_threshold = threshold, .dram_reserve = reserve);
switch (where) {
case PLACE_DRAM: /* internal */ break;
case PLACE_PSRAM: /* external */ break;
case PLACE_FAIL: /* neither will take it */ break;
}
Arguments for every exter call, where each call reads only the members it needs.
const size_t size

The two-buffer index is separate, and acts on a PingPong the caller owns:

EMBED_CALL(exter.pingpong_init, ExternaCfg, .pingpong = &pair);
const uint8_t filling = EMBED_CALL(exter.pingpong_fill, ExternaCfg, .pingpong = &pair);
const uint8_t draining = EMBED_CALL(exter.pingpong_drain, ExternaCfg, .pingpong = &pair);
const uint8_t now = EMBED_CALL(exter.pingpong_swap, ExternaCfg, .pingpong = &pair);
PingPong *const pingpong

Gotchas

dma_required is not advisory. On parts where the DMA engine cannot address external memory, passing EMBED_TRUE stops the answer being PLACE_PSRAM.

PLACE_FAIL is a real answer, not an error code. Handle it.

Reference

Generated reference


Proximus operor: load and store

Three access strategies. Not one thing under three names.

The three

One dispatch table, proxim (src/proximus_operor/proximus_operor.h:218). The strategy is in the entry name and not in a table of its own, and the may-alias part is in the type every entry moves and not in a third entry to pick.

entry strategy use when
load, put unaligned the address may be anything
al_load, al_put aligned you know the alignment holds
mmgr_migro_word may alias the type the above move. A load cannot be reordered against a store of another type
const mmgr_migro_word any = EMBED_CALL(proxim.load, ProximusCfg, .at = p);
const mmgr_migro_word ali = EMBED_CALL(proxim.al_load, ProximusCfg, .at = p);
const uint32_t narrow = EMBED_CALL(proxim.load32, ProximusCfg, .at = p);
Arguments for the proxim calls.
const void *const at

Why they are not merged

Merging them is a miscompile the compiler cannot report.

An aligned load emitted for an address that is not aligned faults on some machines and silently returns a wrong value on others. A load without the may-alias marking lets the optimizer reorder it against a store it genuinely conflicts with, and the reordering is legal, because you told the compiler the pointers could not alias.

This is also why tools/dev_env/names.tsv keeps three infixes for one module. Collapsing them onto one stem merged seven symbol pairs, including the aligned and the unaligned load.

Gotchas

aequus does not check. It is a promise you are making and not a request to verify.

Unaligned is not always slower. On x86-64 proxim.u32 is one mov. The distinction matters most on parts where it is not.

Reference

Generated reference · Width, alignment and byte order