|
jam 0.0.1
A compacting generational garbage collector for C++26
|
Collection stops mutation, marks reachable allocations, computes forwarding addresses, then moves the bytes and rewrites pointers. Objects carry no GC header. Type information arrives through roots and traced edges; pending jobs hold offsets and tracing callbacks, not references into a worker's stack.
Weak associations add an ordered scan after ordinary marking. The scan retains conditional values or queues finalizers, draining marking after each decision. Registry entries and pending/running callbacks are forwarded before metadata reuse. Callbacks run after the collector releases its mutation guard.
Allocation and liveness use eight-byte cells. Each 256-byte rank block has 16 bytes of metadata: live cells, a forwarding base and pointer slots. Alignment adds one byte per 512 bytes. Records may require 8-, 16-, 32- or 64-byte alignment. Dilation retains the necessary alignment groups, including padding. Those cells count toward used space, but the live mask keeps the exact claims throughout forwarding. The prefix pass writes each joined group's effective alignment into the existing alignment nibbles. Each consumer derives the dilated mask in registers; no second liveness bitmap is needed.
Forwarding checks the target's exact claim and clears unclaimed targets. For a survivor it combines the block's destination base with the count of earlier retained cells, including alignment padding. Strong and weak fields share the pointer mask: marking follows only strong edges, so forwarding needs no weak tag. The prefix pass respects alignment and records spanning blocks before workers forward arbitrary pointers. Pointer masks distinguish offsets from data, including the two possible 32-bit pointer fields in each cell. Cells retained only for alignment are copied as data; stale pointer declarations in them are discarded before dilation.
Typed collection rebuilds pointer declarations each time it traces an allocation, so dynamic layouts can change which fields are pointers. Minor collection leaves old allocations untraced and uses the remembered source slots instead.
Forwarding finishes for both generations and all roots before metadata is reused. Pointer masks then pack in place, consuming each source descriptor before clearing or writing its destination. Young masks append to old during promotion or major collection, preserving any shared boundary block. Reclaimed metadata is cleared; collection needs no fresh destination table unless the arena itself grows.
Each heap selects a compactor from CPU and OS capabilities: BMI2+AVX512 (with a VPOPCNTDQ variant), BMI2+AVX2, NEON, or baseline. Workers forward pointer fields and pack live cells using that implementation. Stores write only the live prefix, so adjacent workers do not overstore. The x86 variants use BMI2 to pack pointer masks; other variants use a table. Optional instructions stay inside the selected implementation.
Each marker owns its local queue. Between jobs and at visit.poll(), it processes overdue donation attempts, advancing the deadline by exponential intervals with a 30-microsecond mean. A successful attempt donates the older half of the queue to a randomly selected idle worker. A chain has no independent branch to donate; a tree usually does.
The donor reserves an idle mailbox with CAS, writes the batch, and publishes with release ordering. The recipient acquires it. Returning to idle releases the inbox for reuse. Termination accounting includes transfers in flight. Local queue operations need no synchronization; claims on shared heap metadata still do.
This follows the sender-initiated work in Acar, Charguéraud and Rainey and PASL. The current walker uses neither stack capture nor scheduling exceptions.
Each generation has two adjacent coherent views of its circular backing. Payload access uses the contiguous view; rotation and mapping setup handle wrapping. Compaction steps backward into the reserve. A bounded window prevents workers from getting far enough ahead to overwrite unread source pages. Completed pages release credit to advance the window.
Growth and shrinking remap surviving pages rather than copying their contents. The reserved maximum keeps old below young while the active rings change size.
Windows uses pagefile sections and VirtualAlloc2/MapViewOfFile3 placeholders. Placeholder replacement supports OS pages (4 KiB on x64); the 64 KiB allocation granularity does not force 64 KiB heap pages. Dead pages receive advisory MEM_RESET. A partly retained section keeps its commit charge until its final view and handle are released, so shrinking need not immediately reduce commit.
The default carrier is thread_local. On AArch64, JAM_CONTEXT_X28=ON reserves x28; CMake checks support and propagates -ffixed-x28. All participating code must reserve it. The register has no initial null guarantee: use implicit operations inside a scope, and establish a scope at foreign callback entry from an explicitly saved heap identity. Independent users cannot keep different active contexts in that same register.
heap::current() is pure; scope changes are ordinary stores visible to the compiler. Dereferencing a pointer loads the selected heap's current generation view. Scopes provide no thread synchronization.
Compiler fences bracket collection; worker completion supplies the inter-thread synchronization. These fences add no hardware fence instructions and do not make concurrent mutation safe. Stop mutators and root-set changes before collecting. Trace hooks must tolerate concurrent invocation and must not wait for other hooks.
The raw clear_marks, claim, mark, pointer and compact operations expose the same machinery for custom schedules. Join markers before compaction. Raw heap::compact requires no weak associations or pending/running finalizers; use typed collection to process those registrations. collect(trace), collect_major(trace) and collect_minor(trace, promote) accept a noexcept callback for untyped roots and edges. The callback must claim complete records and enumerate their fields. Omitting the callback requires typed roots and edges throughout. collect(trace) follows the same countdown as collect().