# How this Default_Allocator differs from Jai's This module replaces Jai's `modules/Default_Allocator` with the rpmalloc 2.0.1 port from `my_modules/rpmalloc` (installed with its `install.jai`). Jai finds the default allocator by module name, so the replacement is the directory that comes first on the import path. This document lists everything that behaves differently. Facts about Jai's module refer to Jai beta 0.2.030. Everything here is tested by the port's suite, `my_modules/rpmalloc/tests`: - `jai build.jai - swap` installs this module with `install.jai`, builds a program whose only allocator is the installed copy, runs that program, and type-checks it for macOS and Linux. - The port's own suite (89 tests) covers the allocator code itself. ## 1. Which allocator you get | Platform | Jai's Default_Allocator | This module | |---|---|---| | Windows x64 | Stripped-down rpmalloc 1.4.4: one heap shared by every thread behind one lock, and no thread caches | rpmalloc 2.0.1 (port), one heap per thread (**tested**) | | Linux x64 | The same as on Windows x64 | The same as on Windows (**type-checked only**; there is no Linux machine) | | macOS ARM64 | A thin wrapper over the C runtime's `malloc` / `realloc` / `free` | rpmalloc 2.0.1 (port) (**tested**) | | macOS x64, Linux ARM64 | Linux ARM64: C runtime wrapper; macOS x64: stripped-down rpmalloc 1.4.4 | Builds, but untested | | PS5, Switch 2, iOS, Android, WASM, 32-bit | Their own branches (rpmalloc 1.4.4 on PS5 and Switch 2; the C runtime wrapper elsewhere) | **Not supported.** The module stops with an `#assert`. Take `my_modules` off the import path for these targets. | The CAPS string tells you which allocator is in use: `"rpmalloc 2.0.1 (Jai port) as Default_Allocator"`. Jai's reports `"stripped-down rpmalloc 1.4.4 intended as Default_Allocator"`, or `"Jai wrapper for C’s malloc/free/realloc functions."`. ## 2. Exported interface | Name | Jai's | This module | |---|---|---| | `allocator` | Yes (rpmalloc branch only) | Yes | | `allocator_proc` | Yes | Yes, same signature | | `unsafe_reset()` | rpmalloc branch: forgets all state and starts over. C runtime branch: does nothing. | Releases every lock and keeps all state (§6) | | `initialize_if_needed() -> bool` | Yes | Yes | | `is_initialized() -> bool` | Yes | Yes | | `finalize()` | Yes | Yes (the port's `finalize`) | | `usable_size(ptr) -> s64` | Yes | Yes | | `is_this_yours(heap, ptr) -> bool` | Walks the heap's span lists. It misses older large blocks (finding F4/M2, test HEAP-05) and the blocks of exited threads (HEAP-10). With a null heap it looks only in the one default heap. | Looks the address up in a bitmap of live spans and never reads memory it does not own. With a null heap it checks every thread heap (not first-class heaps). | | `heap_free_all(heap)` | Yes | Yes | | `global_statistics()` | A placeholder struct of 7 fields, always zero | rpmalloc's `Global_Statistics` (9 fields). It is zero unless `ENABLE_STATISTICS`. The memory committed now is `active`; `committed` and `decommitted` are running totals that only grow, as upstream. The pointer form `global_statistics(*stats)` also exists. | | `heap_statistics(heap)` | `{sizecache, spancache}` | `{allocated_size, committed_size, mapped_size}`, zero unless `HEAP_STATISTICS` | | `allocator_default_heap` | A global `*Heap` | **Not provided.** There is no single default heap; each thread has its own. | | The rest of the rpmalloc API | No | `initialize`, `thread_collect`, `trim`, `rpmalloc`, `rpaligned_alloc`, `heap_acquire`, and the rest (see `my_modules/rpmalloc/module.jai`) | **Module parameters.** `ENABLE_ASSERTS` and `ENABLE_VALIDATE_ARGS` have the same names and defaults (both `true`). The port's other parameters are added: statistics, decommit, unmap, first-class heaps, the huge cache and test hooks. `Runtime_Support` imports the module without parameters, so the context allocator always uses the defaults. Importing it with other parameters creates a separate instance with its own state; the context allocator is not affected. ## 3. Allocator protocol | Mode | Jai's | This module | |---|---|---| | ALLOCATE / RESIZE / FREE with `data == null` | rpmalloc branch: the one default heap, behind a global lock. C runtime branch: `malloc`. | The calling thread's own heap, with no lock on the fast path. A block freed by another thread is handed back to its owning heap without blocking. | | Out of memory | **Asserts** with asserts on (N1, tests SIZE-01 and SIZE-07) | Returns null | | Negative or impossibly large size | Not caught (F11, SIZE-02 and SIZE-03). The C runtime wrapper panics in debug builds (N5). | Returns null; a refused RESIZE leaves the block intact | | RESIZE `old_size` | Ignored | When the block moves, `old_size` bytes are copied; 0 means the whole old block. **Passing a positive `old_size` smaller than the live data loses the rest.** | | RESIZE to the same size | Can move the block (I1, RE-02) | Keeps it | | A refused RESIZE | Returns the old pointer (L6, RE-04) | Returns null | | STARTUP | Does nothing; returns null | `initialize()`; returns null. `Process/posix.jai` stores this return value in `context.allocator.data`, where null means "the thread's heap". | | SHUTDOWN | Forgets the default heap; later use fails (F3, LIFE-01) | `finalize()`; the next allocation initialises again | | THREAD_START / THREAD_STOP | Do nothing | Set up / release the thread's heap. Both are optional: a thread gets its heap on its first allocation, and the heap is released when the thread exits. Jai's `Thread` module sends these only on Windows (N4). | | CREATE_HEAP | rpmalloc branch: yes. C runtime branch: **asserts**. | Yes (with `FIRST_CLASS_HEAPS`, the default) | | DESTROY_HEAP | Releases the heap; a new heap can then claim the destroyed heap's blocks (F5, HEAP-01). Blocks still in it leak: 1,970 MiB over 190 heaps (HEAP-11). | Frees every block in the heap, then releases it | | IS_THIS_YOURS | rpmalloc branch: see §2. C runtime branch: **asserts**. | See §2 | | CAPS flags | rpmalloc branch: general heap, threads, CREATE_HEAP, FREE, ACTUALLY_RESIZE, IS_THIS_YOURS. C runtime branch: no heaps, no IS_THIS_YOURS. | The same as the rpmalloc branch, on every platform | **First-class heaps.** A heap from CREATE_HEAP is for one thread at a time, as in upstream rpmalloc. Putting it in a Context that several threads copy lets them race on it. ## 4. Threads - **Heap per thread.** Each thread allocates from its own heap, kept in OS thread-local storage (`TlsAlloc`/`FlsAlloc`, or a pthread key). A Context copied to another thread does not share the heap. - **Heaps of exited threads are reused.** A thread's heap is released when the thread exits, even without THREAD_STOP, and the next new thread takes it over. With Jai's module on Windows and Linux, all threads share one heap behind one lock: there is no per-thread state, but every allocation contends for that lock. - **Freeing another thread's block.** A block freed by a thread other than its owner is queued without a lock. Its owner reuses it on that owner's next allocation, or on `thread_collect()`. If the owner has exited, or is idle in `thread_idle()`, the block is freed at once instead. - **Thread pools: call `thread_idle()`.** A pool thread that sleeps makes no allocator calls, so blocks other threads free into its heap, and the heap's free pages, would stay committed until it wakes. Calling `Default_Allocator.thread_idle()` where a pool thread waits for work gives that memory back and parks the heap until the thread's next allocation. Jai's module has no per-thread state, so it needs no such call. - **Partly used pages per thread.** Each heap keeps its own partly used pages, so a program with many threads holds some more memory in partly filled small pages (64 KiB each) than one shared heap would. Larger pages are committed as they fill. ## 5. Memory use - **Retention.** The port keeps freed pages per thread and huge blocks in a cache of up to 128 MiB committed (`HUGE_CACHE_LIMIT_MB`; a program that frees and allocates many huge blocks may run faster with more, holding up to that much while it does). Cached entries idle for a second are decommitted; blocks beyond the budget are unmapped. It decommits free pages only after a delay. Jai's rpmalloc branch returns memory eagerly, and pays page faults for it on reuse (F1, RET-01: about 112 faults per warm cycle). `trim()` returns every free page of the calling thread's heap, plus the whole huge cache, at once. `thread_collect()` does the same for pages past their delay. - **Address space.** Memory is reserved in 256 MiB spans and committed page by page. Virtual size therefore looks large; the committed size is what costs memory. A 128 KiB bitmap for IS_THIS_YOURS is mapped once. - **Measured footprint** (MiB with the benchmark's live set allocated; `my_modules/rpmalloc/tests/results/BENCH-*.md`): | Benchmark | Windows: Jai's | Windows: this | macOS: Jai's (C runtime) | macOS: this | |---|---|---|---|---| | churn_medium | 204 | 170 | 148 | 66 | | churn_large | 157 | 260 | 128 | 7.8 | | churn_huge | 296 | 452 | 28.5 | 4.2 | - **macOS.** The port decommits with `MADV_FREE_REUSABLE`, so freed memory leaves the process's footprint. ## 6. fork() `Process/posix.jai` calls `unsafe_reset()` in the child, then `allocator_proc(.STARTUP)`. Jai's rpmalloc branch forgets its whole state at that point, so every block allocated before the fork leaks in the child. This module instead releases each lock that another thread of the parent might have held: the global heap lock, the heap-creation flag, the huge-cache lock, and an initialisation another thread had started. Everything else stays as it was. The forking thread's heap and its blocks remain valid in the child. Like Jai's, it cannot repair a list another thread was in the middle of changing; that window is a few instructions under one of those locks. `jai build.jai - swap` checks this: without `unsafe_reset` the smoke test hangs, and with it the test passes. ## 7. Compile time Code in `#run` allocates through this module as well; the swap smoke test checks this. Jai's module also works at compile time. ## 8. Build - This module imports `Basic`, `Bit_Operations` and `Atomics`; Jai's imports nothing. - Its code is about 4,000 lines; Jai's rpmalloc branch is about 2,000. - Unsupported platforms fail to compile (§1). - **One Context field.** The module adds `rpmalloc_default_allocator_thread_heap` (a pointer) to every program's `Context`, a cache of the thread's heap, so allocations and frees need no OS thread-local storage call. A second import of the module with other parameters must pass `CONTEXT_HEAP_CACHE = false`, since two imports cannot add the same field. Jai's module adds nothing to the Context. - It works in programs built with `arithmetic_overflow_check = .FATAL`: no code relies on unsigned wraparound. In `my_modules/rpmalloc/tests`, `jai build.jai - swap overflow=fatal` and the port's suite with `overflow=fatal` test this. Both pass on Windows and macOS; Linux is type-checked only. ## 9. Speed Median ns per operation, module defaults, from `my_modules/rpmalloc/tests/results/BENCH-*.md`; lower is better. | Benchmark | Windows: Jai's | Windows: this | macOS: Jai's (C runtime) | macOS: this | |---|---|---|---|---| | pair_64 (alloc + free of 64 B) | 34.3 | 13.1 | 9.7 | 12.2 | | pair_mixed (16..1024 B) | 256 | 15.5 | 23.6 | 13.4 | | churn_medium (1..64 KiB) | 1059 | 51.7 | 74.0 | 53.2 | | churn_large (64 KiB..1 MiB) | 4600 | 32.3 | 122 | 30.9 | | **churn_huge (2..32 MiB)** | 9526 | **20245** | 618 | **4751** | | realloc_double (64 B to 8 MiB) | 184662 | 15492 | 6373 | 6601 | | mt_churn_8 (8 threads) | 515 | 2.3 | 18.0 | 1.9 | | mt_producer (cross-thread frees) | 484 | 46.4 | 122 | 87.3 | On Windows this module is faster everywhere except **churn_huge** (replacing blocks of 2 MiB and up), where it is 2× slower. On macOS, where Jai's module is the system `malloc`, it is level or faster except on pair_64 (0.8×) and churn_huge (7× slower). That gap is shelved in the plan. ## 10. Test results The test suite was run against Jai's module and against the port: | Platform | Jai's Default_Allocator | This module's code (the port) | |---|---|---| | Windows x64 (Jai's: rpmalloc 1.4.4) | 30 pass, 11 fail, 4 crash (41 skipped: capabilities it lacks) | 82 pass; every other test is SKIP or N/A | | macOS ARM64 (Jai's: C runtime `malloc`) | 30 pass, 6 fail, 2 crash (48 skipped) | 83 pass; every other test is SKIP or N/A | What Jai's module fails, by kind: - **Correctness, Windows:** - SIZE-01 and SIZE-07 assert when the OS refuses memory. - SIZE-02 and SIZE-03 accept negative sizes. - RE-02: a same-size resize moves the block. - RE-04: a refused resize returns the old pointer. - HEAP-01: a new heap claims a destroyed heap's blocks. - HEAP-05 and HEAP-10: IS_THIS_YOURS misses blocks it owns. - HEAP-11: destroyed heaps leak their blocks. - LIFE-01: the allocator is unusable after SHUTDOWN. - **Correctness, macOS:** SIZE-02 crashes on a negative size, and RE-04 is the same as on Windows. - **Memory behaviour, Windows:** RET-01, RET-06, RET-10 and RET-14 fail on page faults. Freed memory goes back to the OS and faults in again on every reuse (up to 2,053 faults per 8 MiB cycle). - **Memory behaviour, macOS:** OS-07, RET-02, RET-07, RET-11 and RET-12 fail because the system `malloc` keeps freed memory private (258 of 258 MiB after freeing everything). THR-09: blocks freed by another thread are not reused first. One upstream bug remains open in the port: U15 (POSIX only; at most one heap per exiting thread can leak, in a rare destructor order). See `my_modules/rpmalloc/module.jai`. ## 11. Before you switch a project - Check every `RESIZE` / `realloc` call for a correct `old_size` (§3). Jai's module ignores it; this one relies on it. - Expect a larger footprint on Windows for large and huge blocks, or call `trim()` at quiet points. - Workloads with many huge blocks (2 MiB and up) are slower than with Jai's module (§9). - Supported platforms are Windows and macOS; Linux is type-checked only. - A block must still be freed by the allocator that made it. Each program and DLL compiled with this module has its own instance, just as with Jai's.