The name of the game is speed.
Bar: as close to Rust as possible.
Mako is not a Rust dialect, but on hot paths it must compete with Rust —
not with GC languages. Concurrent and parallel work is first-class and must
stay fast too (crew, fan, channels) — see SPEED.md.
| Principle | Practice |
|---|---|
| Speed first | Prefer the fast design; convenience features stay off the hot path or opt-in |
| No GC | Scope cleanup, hold / share / arena — no stop-the-world tax |
| Native codegen | .mko → C → clang; release -O3 -flto |
| Low-overhead default | Scalar locals and direct calls avoid ownership/refcount synchronization; allocation and synchronization costs stay explicit |
| Explicit cost | Heavier tools (share, channels, crew) are visible when they cost |
| First-class concurrent/parallel | Language keywords, structured joins — not a slow thread-pool package |
| Measure | Prefer ./scripts/bench-vs-go-rust.sh and real workloads over vibes |
Targets backend and systems workloads: arenas for request scope, tight slice/map layouts, single static binaries.
Book: §11 Speed & memory safety · Release how-to: howto/09-release-builds.md.
Do not invent numbers. Re-run locally:
./scripts/bench-vs-go-rust.sh
# optional parsing:
./scripts/bench-vs-go-rust.sh 2>&1 | awk '/=== CPU/,/=== Memory/' | python3 scripts/parse_bench_ns.py
# CI-style gate (fib/slice/map vs Rust; default max 2.0×):
./scripts/bench-gate.sh
./scripts/bench-gate.sh 1.5 # stricter
# GitHub Actions: job "Bench gate vs Rust" on ubuntu-latest
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:
mako 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. |
--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 |
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 |
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.