The mature path compiles through C. LLVM release emits objects directly and
links with embedded lld/runtime inputs, using LLVM’s default<O3> pipeline.
No GC, no interpreter, no VM tax.
LLVM release currently covers scalar CFG and owned strings. On one Apple arm64
box, Fibonacci compiled in 20.1 ms (LLVM) vs 251.9 ms (C), and ran in 146.5 ms
vs 148.9 ms (Mako C), 147.8 ms (hand C), 148.0 ms (Rust). Correctness gate:
scripts/llvm-backend-test.sh. Those numbers are for that workload, not a
ranking of languages.
Performance is something we design for; it isn’t a finished claim. Makori 0.5.13
locks workload-specific budgets in
benchmarks/performance-contract.json
and CI enforces the reproducible subset with
./scripts/performance-contract.sh. A faster-than-Rust statement applies only
to rows marked strict_rust_claim: true; bounded channel send/recv is measured
against Rust but remains a regression-only budget until the runtime closes that
gap.
Bias toward the fast design. Convenience that costs belongs off the hot path
or behind an opt-in. Measure vs hand-C and Rust per workload
(SPEED_SAFE.md); bump baselines when you meant to change
them. Scope cleanup and hold / share / arena — no stop-the-world.
memory-safety-gate is there for a reason. Codegen is native
(.mko → C → clang / LLVM), release -O3 -flto. Locals and direct calls stay
cheap by default; alloc and sync cost show up when you use them. share,
channels, crew are visible when they cost. Concurrent and parallel work is
language-level. If it isn’t in a script with a method, it isn’t a claim.
Aimed at backend and systems work: request arenas, tight slice/map layouts, native binaries.
Integer channels check waiter counts while holding the channel mutex. Buffered enqueue/dequeue skips condition-variable signaling when no peer is blocked and signals one peer when one item or slot becomes available. Close retains broadcast semantics. Unbuffered rendezvous receives broadcast to senders because slot waiters and handoff-acknowledgement waiters share one condition variable; a one-peer signal can wake the wrong predicate and strand the handoff owner. This removes uncontended wake overhead without weakening MPMC synchronization or lifetime guarantees.
Runtime telemetry is pay-for-use in generated C. Programs that call
runtime_stats_json() or runtime_stats_reset() compile with exact atomic task,
channel, and lock counters. Programs that do not use those builtins compile the
counter operations out of the hot path. Native C integrations can force the
instrumented form with -DMAKO_RUNTIME_METRICS=1.
Release --backend native preserves the mature AST-to-Cranelift whole-function
fast paths for recognized Fibonacci and generated slice-reduction kernels.
General programs and all test harnesses continue through ownership-explicit
shared IR. This prevents canonical IR migration from silently discarding proven
CPU transforms while keeping backend selection narrow and deterministic.
On the C backend, cloning an owned heap-backed slice performs an atomic retain and is O(1). Reads continue to share backing storage. The first mutation of a shared slice allocates and copies its live elements, so that detach is O(n); subsequent unique mutations reuse the detached allocation. Borrowed views and pool-backed buffers do not participate in refcounting.
This removes unconditional deep copies from collection-heavy read and pass-through paths without adding a collector. It is not a promise that every slice operation is allocation-free: mutation of shared storage deliberately pays the copy required to preserve value semantics. The native backend uses explicit owned/borrowed tracking across calls and returns; benchmark each backend rather than assuming the C refcount cost model applies to both.
Book: §11 Speed & memory safety · Release how-to: howto/09-release-builds.md.
Don’t invent numbers. Re-run locally:
# Microbenchmarks (fib, slice, map):
./scripts/bench-gate.sh
./scripts/bench-gate.sh 1.5 # stricter threshold
./scripts/performance-contract.sh
# Direct-native parity against Mako C, hand C, and Rust
# (core ≤1.25×; map ≤2.50×; io ≤2.00×; + regression vs baselines JSON):
./scripts/native-bench-gate.sh
# Subset / override:
# MAKO_NATIVE_WORKLOADS="native_map native_io" ./scripts/native-bench-gate.sh
# Baselines: scripts/native-bench-baselines.json (MAKO_NATIVE_REGRESSION=1.15)
# Years-up steady-state (live ownership + RSS stability):
./scripts/long-run-soak.sh
# HTTP accept-loop soak (RSS under concurrent clients):
./scripts/http-long-run-soak.sh
# See docs/LONG_RUNNING.md (years-up soaks for long-running services).
# Optional: MAKO_ALLOCATOR=mimalloc|jemalloc · scripts/pgo-build.sh for PGO.
# Anneal — adaptive opt (traffic feedback, offline PGO): docs/ADAPTIVE_OPT.md · scripts/anneal-cycle.sh
# HTTP throughput (requires wrk or hey):
./scripts/bench-http.sh
# Compiler scaling (cold and cached checks, JSON output):
python3 scripts/bench-compile.py --output out/compile-bench.json
# Include full debug builds (requires the configured C compiler):
python3 scripts/bench-compile.py --build --output out/compile-build-bench.json
The CI performance contract verifies six runtime kernels against matching Rust
programs and parser hot-path smoke budgets. The strict faster-than-Rust claim
set is fib30x5, struct1m, slice100k, map50k, and string20k, all capped
at 1.5× Rust in the hard gate. chan50k is intentionally separate: it has a
3.5× regression budget and is not a faster-than-Rust claim. Broader HTTP
throughput is tracked in scripts/bench-http.sh with the method and minimum
budget recorded in the contract JSON; CI keeps HTTP correctness/RSS as hard
soak gates because load-generator availability and loopback scheduling are
environmental.
Compiler fixtures cover exact 1k, 10k, and 100k source sizes across four
project shapes. CI validates the complete generated matrix on Linux, exercises
a 10k full build there, and records a 1k smoke sample on every platform without
enforcing timing thresholds. See
benchmarks/compile/README.md for the
fixture definitions and focused-run options.
The direct-native gate builds one output-validated workload with the Cranelift backend, the existing C backend, hand-written C, and Rust. It performs warmups, rotates execution order across seven samples, reports medians and binary sizes, and fails when native exceeds the requested ratio. On the 2026-07-20 Apple arm64 development run, native took 350.654 ms versus 170.098 ms for Makori C, 167.629 ms for hand C, and 168.959 ms for Rust: roughly 2.08× slower. This is a failed speed gate, not a publishable “faster than C/Rust” result.
After conservative recursive-addition elimination, a seven-sample follow-up measured direct native at 203.168 ms versus 170.599 ms for Makori C, 167.923 ms for hand C, and 169.065 ms for Rust (1.19–1.21×). Component fixtures isolated the remaining gaps in recursive Fibonacci and slice construction/reduction.
A subsequent checked SIMD reduction pass handles the exact safe loop shape
sum = sum + values[i]; i = i + 1 eight elements at a time using four
independent two-lane accumulators. It enters SIMD only when the entire batch is
below the source bound and actual slice length, then uses
the ordinary checked scalar path for odd tails or invalid ranges. In a separate
21-sample rotated component run under a slower thermal state, native measured
30.678 ms versus 26.430 ms for C and 25.924 ms for Rust (1.16–1.18×), improving
the earlier slice ratio of 1.21–1.24×. A noisy nine-sample combined run remained
1.19–1.23× behind, so the strict gate still fails. Widening the reduction from
one to four independent two-lane accumulators produced a further 3.2% direct
A/B kernel improvement (22.339 ms versus 23.069 ms across 41 rotated samples).
Conservative interval analysis also selects unsigned constant remainder for
nonnegative, nonoverflowing recurrences such as x = (x*c) % m; a signed-control
A/B measured a further 1.2% kernel improvement. Negative or overflow-possible
expressions retain signed remainder semantics.
Exact recognition of the canonical fib(n - 1) + fib(n - 2) recurrence uses
wrapping fast doubling, preserving the source result while reducing exponential
work to logarithmic work. For slices, a strict proof removes append growth checks
from make([]T, 0, n) fill loops. When that local slice then feeds only a sum and
never escapes, producer/reduction fusion removes the allocation and second memory
pass entirely. Proven nonnegative Mersenne-modulus recurrences use fold and
conditional subtraction instead of division.
The gate now measures the combined workload and both components independently, plus compile latency, compiler/runtime peak RSS, and binary size. A seven-sample Apple arm64 run on 2026-07-20 measured the slice component at 11.953 ms native, 20.246 ms Mako C, 18.359 ms hand C, and 18.516 ms Rust (0.590–0.651×). Native runtime RSS was 0.135–0.151×, source-to-binary latency was 58.973 ms versus 248.792 ms for the C backend (0.237×), and compiler RSS was 0.451×. Native binaries remained within 1.003× of hand C and smaller than Mako C. All configured gates pass. These are workload-specific results; the LLVM release backend remains necessary for broad optimizing parity.
Full gate on an otherwise idle host. Ratios are median(mako-native) divided by median(baseline), so 1.000 is equal and higher is slower.
| workload | vs baseline C | vs baseline Rust |
|---|---|---|
native_fib |
0.986 | 0.998 |
native_parity |
1.011 | 1.002 |
native_slice |
1.113 | 1.096 |
native_string_slice |
1.154 | 1.252 |
native_map |
1.540 | 0.229 |
native_io |
1.122 | 1.139 |
Compile latency was 68.120 ms for the native backend against 331.082 ms for the C backend, at 0.687× the compiler RSS.
The baselines are the programs in examples/bench/*.c and *.rs. They are
small, single-purpose, and written to do the same work as the Makori version;
they are not tuned implementations and should not be read as a statement about
those languages. The numbers are here to catch regressions between Mako
releases.
Two things worth recording about the method. Sample count matters at this
scale: native_string_slice measured 1.203, 1.333, 1.356 and 1.415 against the
Rust baseline across four seven-sample runs, then settled at 1.252 with
twenty-five. That spread is wide enough to report a regression that is not
there, and one was reported this session before being disproved by rebuilding
the previous compiler and measuring both under the same conditions (5.27 ms
before the change, 5.07 ms after). The gate default is now fifteen samples.
The remaining gaps look structural rather than accidental. mako_str_clone is
already malloc plus memcpy plus a terminator, and codegen already passes
string literals as borrowed views instead of allocating them, so the
string_slice gap is not redundant work — it is a 16-byte length-carrying
MakoString against an 8-byte pointer, which also accounts for the RSS
difference (9.5 MB against 3.8 MB). Inline storage for short strings would
remove the allocation entirely; that is an ABI change across the runtime and
has not been attempted. The native_map gap works out to roughly 1.4 ns per
operation over two million operations, which is consistent with the per-set
growth check and tombstone bookkeeping that a fixed-size baseline does not need
to do. A pre-sized map that cannot grow or delete would close it; micro-tuning
the existing one probably will not.
IDs on the hot path: Uuid / ULID are 16-byte Copy POD (stack, no GC).
Prefer uuid_v7 / ulid_new for time-ordered keys; format to string only at
API boundaries. uuid_from_bytes hard-fails on wrong length (memory safety).
Do not publish a throughput number by itself. Any req/sec claim must include the test setup and the exact command used to produce it. At minimum, include:
makori build flags, C compiler, optimization flags, and
linked optional libraries.Do not publish a throughput number until every field above comes from an actual run. If any details are missing, describe the number as an informal local experiment, not a benchmark result.
Wall ns for each kernel (now_ns). Lower is better.
black_box prevents LTO from erasing work.
| Kernel | Wall time |
|---|---|
| fib30×5 | 1.42 ms |
| slice100k append | 61 µs |
| map50k pre-sized | 503 µs |
These are narrow local microbenchmarks, not proof of end-to-end service performance. Use them to sanity-check codegen changes, then measure your own service workload with the methodology above.
Peak RSS via /usr/bin/time -l may be unavailable in restricted sandboxes; run the
script on a normal shell for RSS lines.
| Profile | Flags | Use |
|---|---|---|
| Debug (default) | -O0 -g |
Dev, tests, ASan (--sanitize=address) |
Release (--release) |
-O3 -flto -DNDEBUG |
Optimized native build; safe indexing remains checked |
| Optional strip | MAKO_STRIP=1 |
Smaller deploy artifacts |
Release object-cache fingerprints include the selected optimization mode and
C compiler identity. Builds using MAKO_CFLAGS or PGO (MAKO_PGO_GEN /
MAKO_PGO_USE) bypass incremental object/typecheck reuse because external
headers and profile contents can change without changing .mko sources.
mako profile builds and runs one program, then reports frontend, backend,
build, run, total wall time, and exit code. --json emits the stable
mako.profile.v1 schema for CI trend collection.
mako build --release main.mko -o svc
mako profile main.mko --release --json
These are built into the runtime and codegen — no user action required.
| Optimization | What it does |
|---|---|
| wyhash | Map key hashing processes 8 bytes at a time (replaced byte-by-byte FNV-1a). Uses 128-bit multiply mixing. |
| Stack f-strings | String interpolation uses a 256-byte stack buffer. Short f-strings never malloc. |
| Constant folding | 1 + 2, n > 0 with literal operands fold to constants at compile time. |
| Zero-copy comparisons | x == "literal", str_eq, str_has_prefix, str_has_suffix, str_contains, match arms, and print with string literals all point into read-only data instead of allocating. |
| HTTP header switch | Header interning dispatches by name length, skipping non-matching headers. |
| Atomic conn count | Active HTTP connections tracked with an atomic counter, not a linear scan. |
Lock-free chan_cap |
Channel capacity is immutable — reads skip the mutex entirely (int ring; ptr/str have matching helpers). |
chan_len / chan_cap any T |
Typecheck + codegen for struct/tuple/string/enum channels — not only chan[int]. |
select condvar |
Channel select waits on a shared condition variable; send/close broadcast wakeups (no 2 ms poll). |
| Codegen monomorph cache | want_map checks use a joined key set, eliminating per-call heap allocation. |
Codegen emit_line |
Hot emission writes with format_args! into the output buffer — no per-line String. |
| Stack POD array lits | [a,b,c] for int/float/bool/byte → stack buffer + cap==0 view (no malloc/free). Escape heapifies. |
| Empty slices | [] / make([],0,0) → no heap until first grow. |
| Cold free | Slice free is MAKO_UNLIKELY(cap>0) — views and stack lits cost a predicted-not-taken branch. |
Zero-alloc print(f"...") |
print, log_info, log_warn, log_error, log_debug with f-string args use finish_view — no malloc/free, the stack buffer is consumed directly. |
Zero-alloc http_respond(f"...") |
http_respond and http_respond_json with f-string body use finish_view — response body built on stack, written to socket, no heap allocation. |
writev print |
print uses a single writev syscall (data + newline) instead of fwrite + fputc + fflush (Unix). |
| Map probe hints | Map get/has use MAKO_LIKELY(FULL) branch hints — first-probe hits skip the tombstone/empty check. |
--release for anything you measure or ship.make([]int, 0, n), make(map[int]int, n).
Map monomorph C helpers are demand-driven (only used map[K]V shapes
are emitted) — still prefer fewer distinct map shapes on hot modules.hold over share when unique ownership works (no RC traffic).now_ns / black_box for microbenches (ms timers hide wins).| Win | Effect |
|---|---|
now_ns + black_box |
Honest ns benches; LTO-safe |
| Map II/SI/SS pre-size to ~75% load | Fewer rehashes on sequential insert |
| Map rehash move (not clone) | Less alloc/CPU on grow |
Map set/get MAKO_LIKELY paths |
Better branch prediction on hits |
Slice/byte make: zero only len, not unused cap |
Less CPU + cleaner pages |
Fast-path append when len < cap (+ likely) |
One branch, no realloc check math |
| Copy-on-write append preserves spare capacity | Shared slices detach at the existing capacity and grow only when len + 1 > cap, avoiding exponential capacity inflation across aggregate calls |
HTTP: mako_arena_cstr / arena_text_n |
No malloc+arena double copy |
Empty string singleton + mako_str_free |
No malloc for ""; safe free of singleton |
str_clone / str_concat empty fast paths |
Less allocator traffic |
| Safe release indexing | Safe bounds checks remain enabled; the C optimizer can remove checks it proves redundant |
| Checked SIMD int reductions | Proven sum += []int[i] loops consume eight-element batches with fixed-width SIMD; runtime source-bound and slice-length proofs preserve scalar checked fallback |
| Proven unsigned remainder | Nonnegative, nonoverflowing modular recurrences use shorter unsigned constant reduction; CFG merges discard interval facts conservatively |
| Borrowed int slicing | Named []int slices are allocation-free views; owned temporary slicing copies once so the original can be freed without dangling pointers |
| Non-escaping slice fusion | A proven append-only producer followed by a sole sum consumer becomes one allocation-free loop |
Debug and release builds abort on out-of-bounds safe indexing. The C optimizer
can still remove checks it proves redundant. unsafe { ... } and
unsafe_index are the explicit, reviewed opt-out for a proven index invariant.
To keep checks in production:
mako build --release main.mko -o svc
# or in mako.toml:
# [profile.release]
# bounds_checks = "on"
Prefer debug + ASan while developing. See SECURITY.md.
| Issue | Impact | Fix |
|---|---|---|
| Safe release checks were elided by default | Out-of-bounds safe code could become undefined behavior | Safe checks are retained; explicit unsafe is the opt-out |
Empty mako_str_from_cstr("") always malloc |
Alloc pressure on empty strings | Process-wide empty singleton |
| Map grow at 70% load | Extra rehash on dense inserts | Grow at ~75% (4/3 pre-size) |
| No branch hints on map/append | Mispredict on hot loops | MAKO_LIKELY / UNLIKELY |
map[string]… always cloned keys |
Alloc per insert | map_si_set_take / map_ss_set_take (move) |
| HTTP parse N× arena copies | Method/path/headers/body each copied | Views into one conn.raw buffer |
ch.send(s) always clones string |
Alloc per message | chan_str_send_take / chan_str_try_send_take |
| JSON respond malloc'd Content-Type | Alloc per reply | Interned static application/json; charset=utf-8 |
| Proxy socket pump small chunks | Extra syscalls | Linux splice 256 KiB + F_SETPIPE_SZ; file→socket sendfile |
Still intentional costs (visible when you use them): share RC, channel sync,
default m[k]=v still clones string keys (safe), default ch.send(s) clones
(safe), kick heap-box for multi-word types.
// Default: clone key (key still usable)
m["k"] = 1
// Hot path: move ownership of an owned string into the map (no second alloc)
map_si_set_take(m, owned_key, 1) // map[string]int
map_ss_set_take(m, owned_k, owned_v) // map[string]string
Rehash already moves owned keys (no clone/free thrash).
let ch = chan_open[string](64)
// Default: clone so caller retains s
let _ = ch.send(s)
// Hot path: move owned temporary (no second alloc)
let _ = chan_str_send_take(ch, owned_msg)
// Non-blocking: 1 queued, 0 full/closed — always consumes the string
let ok = chan_str_try_send_take(ch, owned_msg)
Prefer region builtins over allocating s[i:j] when you only need to compare or
search:
// Good: no temporary string
if str_slice_eq(line, 0, 3, "GET") == 1 { ... }
let comma = str_slice_index(row, 0, len(row), ",")
if str_at_eq(path, 0, "/api/") == 1 { ... }
let b = str_byte_at(s, i) // 0..255 or -1
Same idea as str_eq / str_contains, scoped to a byte range. Applies to CSV,
paths, config lines, log parsing, wire formats — any general text work.
http_fill_conn / http_parse_request store method, path, body, Host, User-Agent,
Content-Type as views into the connection’s durable raw[] buffer (one
memcpy of the request). Common Content-Type values and header names are
interned to static views (application/json, Host, …) so compares/responses
need no per-request malloc. respond_json uses the interned JSON type.
Do not free view strings; clone if you need them after the connection reuses the
buffer (http_next).
tcp_fd_copy / tcp_splice: Linux uses kernel splice (256 KiB chunks, enlarged
pipe via F_SETPIPE_SZ) for socket↔socket. Apple/FreeBSD try sendfile for
regular file→socket, then fall back to a 64 KiB userspace pump.
crew / channels: pthread sync, no STW GC. Prefer bounded channels + request arenas.
Hot rebuilds: changed units + link only — BUILD.md.