examples: lisp — chunked bump arena for value/env cells
Replaces rt_alloc-per-cell (one 4 KiB mmap each) with arena_alloc over 64 KiB chunks. test_huge peak under massif --pages-as-heap=yes drops from ~525 MB to ~253 MB. Same lifetime semantics; remaining bulk is per-call append() in eval's arg slice (rt_ensure still mmaps page-per-call).
This commit is contained in:
@@ -115,14 +115,17 @@ env where `od?` is unbound; the tie-back only adds the self-binding.
|
|||||||
|
|
||||||
## Interpreter limitations (design, not bug)
|
## Interpreter limitations (design, not bug)
|
||||||
|
|
||||||
- **No GC.** Every cons / value / env frame is `mmap`'d via
|
- **No GC.** Cons / value / env cells come from a single bump-
|
||||||
`rt_alloc` and never reclaimed. A long REPL session leaks until
|
pointer arena (`arena_alloc`, 64 KiB chunks chained via `os.alloc`).
|
||||||
the process exits. The test_huge demo peaks at ~595 MB under
|
The arena never reclaims — long REPL sessions still leak — but
|
||||||
`--pages-as-heap=yes`. Per-top-level-form arena reset would cut
|
cells pack tightly instead of one cell per 4 KiB mmap. test_huge
|
||||||
this by ~100× — see the closing note in `repl()` for the hook
|
peaks at ~253 MB under `--pages-as-heap=yes`, down from ~525 MB
|
||||||
point. Note: `rt_alloc` is one mmap syscall per call returning a
|
pre-arena. The remaining bulk is `append(xs, av)` in eval's arg
|
||||||
whole 4KB page, so even tail-recursive loops are bottlenecked on
|
loop: every apply allocates a fresh `[]*value` through `rt_ensure`,
|
||||||
allocation, not on Lisp work — `(spin 100000 0)` runs in ~4s.
|
which still hits `rt_alloc` page-per-call. Per-top-level-form
|
||||||
|
arena reset (perm/trans split + Cheney promote on define/set!) is
|
||||||
|
the next step — see the closing note in `repl()` for the hook
|
||||||
|
point.
|
||||||
- **No bigints.** `i64` wraps silently on overflow. `(fact 21)`
|
- **No bigints.** `i64` wraps silently on overflow. `(fact 21)`
|
||||||
rolls over.
|
rolls over.
|
||||||
- **Float printing is fixed `%.6f`.** `1.0` prints as `1.000000`.
|
- **Float printing is fixed `%.6f`.** `1.0` prints as `1.000000`.
|
||||||
|
|||||||
@@ -10,8 +10,9 @@
|
|||||||
// (*value | parserr | eof) from the parser
|
// (*value | parserr | eof) from the parser
|
||||||
// - `match` with `yield` so the REPL dispatch is one expression
|
// - `match` with `yield` so the REPL dispatch is one expression
|
||||||
// - `?` to propagate the error variant up the call chain
|
// - `?` to propagate the error variant up the call chain
|
||||||
// - struct types + struct literals + the `alloc(structlit{...})`
|
// - struct types with manual field-store init through *T (the
|
||||||
// constructor sugar (subject to the f64/str traps below)
|
// `alloc(structlit{...})` sugar trips f64/str cgen traps; we
|
||||||
|
// hand-roll instead — see arena_alloc and the v* constructors)
|
||||||
// - enum types (valkind, sform, btin, tkind) + `switch` dispatch
|
// - enum types (valkind, sform, btin, tkind) + `switch` dispatch
|
||||||
// - tagged-union-typed function parameters + bool-return helpers
|
// - tagged-union-typed function parameters + bool-return helpers
|
||||||
// - `[]*value` slices with `append(s, v)` growth
|
// - `[]*value` slices with `append(s, v)` growth
|
||||||
@@ -19,6 +20,8 @@
|
|||||||
// - classic `for (cond)` + `break`
|
// - classic `for (cond)` + `break`
|
||||||
// - `defer` to release the slurp buffer on every exit edge
|
// - `defer` to release the slurp buffer on every exit edge
|
||||||
// - runes ('a'), str literals, f64 arithmetic, mixed i64/f64 promo
|
// - runes ('a'), str literals, f64 arithmetic, mixed i64/f64 promo
|
||||||
|
// - a chunked bump-pointer arena (arena_alloc) over os.alloc, so
|
||||||
|
// every cell isn't paying a 4 KiB-page-per-allocation tax
|
||||||
// - module imports (use os/fmt/ascii/strconv/strings)
|
// - module imports (use os/fmt/ascii/strconv/strings)
|
||||||
//
|
//
|
||||||
// Build: make
|
// Build: make
|
||||||
@@ -104,6 +107,62 @@ export type env = struct {
|
|||||||
};
|
};
|
||||||
def ENV_SZ: u64 = 32u64;
|
def ENV_SZ: u64 = 32u64;
|
||||||
|
|
||||||
|
// ---- arena allocator --------------------------------------------------
|
||||||
|
//
|
||||||
|
// Bump-pointer arena over 64 KiB chunks from os.alloc. Replaces the
|
||||||
|
// page-per-cell waste of routing every value/env through os.alloc
|
||||||
|
// directly — rt_alloc rounds the request up to a 4 KiB page, so each
|
||||||
|
// 96-byte value used to cost 4032 bytes of slack. test_huge.lisp
|
||||||
|
// peaked at ~595 MB under massif --pages-as-heap=yes; the same
|
||||||
|
// allocation pattern now packs into chained 64 KiB chunks.
|
||||||
|
//
|
||||||
|
// Same lifetime semantics as the old direct-mmap path: never reclaimed
|
||||||
|
// inside a single process. A follow-up (the closing note in repl())
|
||||||
|
// will split this into perm/trans and reset the trans arena between
|
||||||
|
// top-level forms.
|
||||||
|
|
||||||
|
def ARENA_CHUNK: u64 = 65536u64; // 64 KiB per mmap
|
||||||
|
def CHUNK_HEADER: u64 = 32u64; // header bytes, 8-aligned
|
||||||
|
|
||||||
|
type chunk = struct {
|
||||||
|
next: *chunk,
|
||||||
|
cur: u64,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Chained list of mmap'd chunks. `arena_head` is the active chunk;
|
||||||
|
// older chunks reachable via .next stay live for the values that
|
||||||
|
// landed in them. arena_init() seeds the first chunk.
|
||||||
|
let arena_head: *chunk = nil;
|
||||||
|
|
||||||
|
fn arena_new_chunk(prev: *chunk) *chunk = {
|
||||||
|
let raw: *u8 = os.alloc(ARENA_CHUNK): *u8;
|
||||||
|
let c: *chunk = raw: *chunk;
|
||||||
|
c.next = prev;
|
||||||
|
c.cur = 0u64;
|
||||||
|
return c;
|
||||||
|
};
|
||||||
|
|
||||||
|
export fn arena_init() void = {
|
||||||
|
arena_head = arena_new_chunk(nil);
|
||||||
|
};
|
||||||
|
|
||||||
|
// arena_alloc — bump `n` bytes off the head chunk; mmap a new chunk if
|
||||||
|
// the head can't fit the request. Result is zeroed memory: fresh mmap
|
||||||
|
// pages start zero, and we don't recycle in this pass.
|
||||||
|
fn arena_alloc(n: u64) *void = {
|
||||||
|
let n8: u64 = (n + 7u64) & ~7u64;
|
||||||
|
let h: *chunk = arena_head;
|
||||||
|
if (h.cur + n8 + CHUNK_HEADER > ARENA_CHUNK) {
|
||||||
|
let c: *chunk = arena_new_chunk(h);
|
||||||
|
arena_head = c;
|
||||||
|
h = c;
|
||||||
|
};
|
||||||
|
let raw: *u8 = h: *u8;
|
||||||
|
let p: *u8 = raw + CHUNK_HEADER + h.cur;
|
||||||
|
h.cur += n8;
|
||||||
|
return p: *void;
|
||||||
|
};
|
||||||
|
|
||||||
// ---- error variants ---------------------------------------------------
|
// ---- error variants ---------------------------------------------------
|
||||||
|
|
||||||
export type rterror = !str; // runtime: division by zero, unbound symbol, …
|
export type rterror = !str; // runtime: division by zero, unbound symbol, …
|
||||||
@@ -112,23 +171,32 @@ export type eof = !void; // parser: end of input — distinct from a parse
|
|||||||
|
|
||||||
// ---- value constructors ----------------------------------------------
|
// ---- value constructors ----------------------------------------------
|
||||||
//
|
//
|
||||||
// alloc(structlit{...}) sugar: cgen rewrites this to rt_alloc(VALUE_SZ)
|
// Every v* constructor allocates one VALUE_SZ slab from the arena and
|
||||||
// + per-field stores, returning a freshly-init'd *value. Mirrors the
|
// initialises the fields the kind needs. The cgen's
|
||||||
// `new T(...)` ergonomics other languages have without ww needing
|
// `alloc(value{...})` struct-literal sugar lowers to rt_alloc directly
|
||||||
// constructors or generics.
|
// (one mmap per cell), so the constructors hand-roll the alloc + per-
|
||||||
|
// field stores instead. Same shape as `new T(...)` in other languages.
|
||||||
|
|
||||||
export fn vnil() *value = {
|
export fn vnil() *value = {
|
||||||
return alloc(value{ kind = valkind.NIL });
|
let p: *value = arena_alloc(VALUE_SZ): *value;
|
||||||
|
p.kind = valkind.NIL;
|
||||||
|
return p;
|
||||||
};
|
};
|
||||||
|
|
||||||
export fn vbool(b: bool) *value = {
|
export fn vbool(b: bool) *value = {
|
||||||
let n: i32 = 0;
|
let n: i32 = 0;
|
||||||
if (b) { n = 1; };
|
if (b) { n = 1; };
|
||||||
return alloc(value{ kind = valkind.BOOL, bval = n });
|
let p: *value = arena_alloc(VALUE_SZ): *value;
|
||||||
|
p.kind = valkind.BOOL;
|
||||||
|
p.bval = n;
|
||||||
|
return p;
|
||||||
};
|
};
|
||||||
|
|
||||||
export fn vint(v: i64) *value = {
|
export fn vint(v: i64) *value = {
|
||||||
return alloc(value{ kind = valkind.INT, ival = v });
|
let p: *value = arena_alloc(VALUE_SZ): *value;
|
||||||
|
p.kind = valkind.INT;
|
||||||
|
p.ival = v;
|
||||||
|
return p;
|
||||||
};
|
};
|
||||||
|
|
||||||
// f64 → struct-field via a *T pointer is mis-lowered (stores AX instead
|
// f64 → struct-field via a *T pointer is mis-lowered (stores AX instead
|
||||||
@@ -139,7 +207,7 @@ let fbuf: f64 = 0.0;
|
|||||||
|
|
||||||
fn vfloat(v: f64) *value = {
|
fn vfloat(v: f64) *value = {
|
||||||
fbuf = v;
|
fbuf = v;
|
||||||
let p: *value = os.alloc(VALUE_SZ): *value;
|
let p: *value = arena_alloc(VALUE_SZ): *value;
|
||||||
p.kind = valkind.FLOAT;
|
p.kind = valkind.FLOAT;
|
||||||
let raw: *u8 = p: *u8;
|
let raw: *u8 = p: *u8;
|
||||||
let dst: *u8 = raw + FVAL_OFF;
|
let dst: *u8 = raw + FVAL_OFF;
|
||||||
@@ -151,34 +219,44 @@ fn vfloat(v: f64) *value = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export fn vsym(id: i32) *value = {
|
export fn vsym(id: i32) *value = {
|
||||||
return alloc(value{ kind = valkind.SYM, sid = id });
|
let p: *value = arena_alloc(VALUE_SZ): *value;
|
||||||
|
p.kind = valkind.SYM;
|
||||||
|
p.sid = id;
|
||||||
|
return p;
|
||||||
};
|
};
|
||||||
|
|
||||||
// alloc(value{ text = s }) writes only s.ptr — the BX register holding
|
// Manual init via `p.text = s` writes both halves of the str slice;
|
||||||
// s.len is clobbered by the new-ptr reload before the .len store.
|
// the cgen's struct-literal path drops s.len because BX gets reloaded
|
||||||
// Manual init writes both halves correctly.
|
// with the new-pointer before the .len store.
|
||||||
fn vstr(s: str) *value = {
|
fn vstr(s: str) *value = {
|
||||||
let p: *value = os.alloc(VALUE_SZ): *value;
|
let p: *value = arena_alloc(VALUE_SZ): *value;
|
||||||
p.kind = valkind.STR;
|
p.kind = valkind.STR;
|
||||||
p.text = s;
|
p.text = s;
|
||||||
return p;
|
return p;
|
||||||
};
|
};
|
||||||
|
|
||||||
export fn vcons(a: *value, d: *value) *value = {
|
export fn vcons(a: *value, d: *value) *value = {
|
||||||
return alloc(value{ kind = valkind.CONS, car = a, cdr = d });
|
let p: *value = arena_alloc(VALUE_SZ): *value;
|
||||||
|
p.kind = valkind.CONS;
|
||||||
|
p.car = a;
|
||||||
|
p.cdr = d;
|
||||||
|
return p;
|
||||||
};
|
};
|
||||||
|
|
||||||
fn vbuiltin(id: i32) *value = {
|
fn vbuiltin(id: i32) *value = {
|
||||||
return alloc(value{ kind = valkind.BUILTIN, ival = id: i64 });
|
let p: *value = arena_alloc(VALUE_SZ): *value;
|
||||||
|
p.kind = valkind.BUILTIN;
|
||||||
|
p.ival = id: i64;
|
||||||
|
return p;
|
||||||
};
|
};
|
||||||
|
|
||||||
fn vlambda(params: *value, body: *value, e: *env) *value = {
|
fn vlambda(params: *value, body: *value, e: *env) *value = {
|
||||||
return alloc(value{
|
let p: *value = arena_alloc(VALUE_SZ): *value;
|
||||||
kind = valkind.LAMBDA,
|
p.kind = valkind.LAMBDA;
|
||||||
car = params,
|
p.car = params;
|
||||||
cdr = body,
|
p.cdr = body;
|
||||||
envp = e,
|
p.envp = e;
|
||||||
});
|
return p;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- symbol interner --------------------------------------------------
|
// ---- symbol interner --------------------------------------------------
|
||||||
@@ -626,7 +704,10 @@ fn parse_expr(L: *lexer) (*value | parserr | eof) = {
|
|||||||
// ---- env --------------------------------------------------------------
|
// ---- env --------------------------------------------------------------
|
||||||
|
|
||||||
fn env_define(e: **env, sid: i32, v: *value) void = {
|
fn env_define(e: **env, sid: i32, v: *value) void = {
|
||||||
let f: *env = alloc(env{ sid = sid, val = v, next = *e });
|
let f: *env = arena_alloc(ENV_SZ): *env;
|
||||||
|
f.sid = sid;
|
||||||
|
f.val = v;
|
||||||
|
f.next = *e;
|
||||||
*e = f;
|
*e = f;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1072,11 +1153,10 @@ export fn eval(v0: *value, ein: **env) (*value | rterror) = {
|
|||||||
// env that closures (e.g. `(adder 5)` returning
|
// env that closures (e.g. `(adder 5)` returning
|
||||||
// `(lambda (x) (+ x k))`) had captured.
|
// `(lambda (x) (+ x k))`) had captured.
|
||||||
if (bv.kind == valkind.LAMBDA) {
|
if (bv.kind == valkind.LAMBDA) {
|
||||||
let frame: *env = alloc(env{
|
let frame: *env = arena_alloc(ENV_SZ): *env;
|
||||||
sid = nameval.sid,
|
frame.sid = nameval.sid;
|
||||||
val = bv,
|
frame.val = bv;
|
||||||
next = bv.envp,
|
frame.next = bv.envp;
|
||||||
});
|
|
||||||
bv.envp = frame;
|
bv.envp = frame;
|
||||||
};
|
};
|
||||||
return vnil();
|
return vnil();
|
||||||
@@ -1122,9 +1202,10 @@ export fn eval(v0: *value, ein: **env) (*value | rterror) = {
|
|||||||
return "let: name must be sym": rterror;
|
return "let: name must be sym": rterror;
|
||||||
};
|
};
|
||||||
let val = eval(pair.cdr.car, &cure)?;
|
let val = eval(pair.cdr.car, &cure)?;
|
||||||
let f: *env = alloc(env{
|
let f: *env = arena_alloc(ENV_SZ): *env;
|
||||||
sid = nm.sid, val = val, next = inner,
|
f.sid = nm.sid;
|
||||||
});
|
f.val = val;
|
||||||
|
f.next = inner;
|
||||||
inner = f;
|
inner = f;
|
||||||
bcur = bcur.cdr;
|
bcur = bcur.cdr;
|
||||||
};
|
};
|
||||||
@@ -1186,9 +1267,10 @@ export fn eval(v0: *value, ein: **env) (*value | rterror) = {
|
|||||||
if (nm.kind != valkind.SYM) {
|
if (nm.kind != valkind.SYM) {
|
||||||
return "lambda: bad param": rterror;
|
return "lambda: bad param": rterror;
|
||||||
};
|
};
|
||||||
let f: *env = alloc(env{
|
let f: *env = arena_alloc(ENV_SZ): *env;
|
||||||
sid = nm.sid, val = xs[i], next = inner,
|
f.sid = nm.sid;
|
||||||
});
|
f.val = xs[i];
|
||||||
|
f.next = inner;
|
||||||
inner = f;
|
inner = f;
|
||||||
p = p.cdr;
|
p = p.cdr;
|
||||||
i += 1;
|
i += 1;
|
||||||
@@ -1318,6 +1400,7 @@ fn bind_one(e: **env, name: str, id: i32) void = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export fn initsyms() void = {
|
export fn initsyms() void = {
|
||||||
|
arena_init();
|
||||||
sym_off = os.alloc((SYM_CAP: u64) * 4u64): *i32;
|
sym_off = os.alloc((SYM_CAP: u64) * 4u64): *i32;
|
||||||
sym_len = os.alloc((SYM_CAP: u64) * 4u64): *i32;
|
sym_len = os.alloc((SYM_CAP: u64) * 4u64): *i32;
|
||||||
sym_blob = os.alloc(SYM_BLOBSZ: u64): *u8;
|
sym_blob = os.alloc(SYM_BLOBSZ: u64): *u8;
|
||||||
|
|||||||
Reference in New Issue
Block a user