Skip to content

perf: cache the recomputable half of a warm boot; standard-library boot image (#46) - #57

Merged
pyrex41 merged 4 commits into
mainfrom
perf/boot-image-46
Aug 11, 2026
Merged

perf: cache the recomputable half of a warm boot; standard-library boot image (#46)#57
pyrex41 merged 4 commits into
mainfrom
perf/boot-image-46

Conversation

@pyrex41

@pyrex41 pyrex41 commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Warm start-up was ~0.16 s and, as issue #46's last re-measurement established, none of it was compilation — the kernel bytecode cache and the user fasl cache were both hitting. The time went on state that the caches had no way to express and therefore rebuilt from scratch on every single start. This PR caches that state.

What was actually being recomputed

Measured on arm64 LuaJIT 2.1.1774638290, warm, os.clock inside the process (a quiet-machine reference boot: require 0.002 + load_kernel 0.046 + initialise 0.095 = 0.143 s):

item cost what it is
klambda/types.kl's 161 (declare …) 43 ms declare runs the type theory for real: shen.variancy under the Prolog machine + (eval-kl (shen.prolog-abstraction Type)), a full KL→Lua compile and loadstring per signature
(shen.build-lambda-table (external shen)) 12 ms shen.lambda-entry runs the whole compiler once per external symbol (~280)
shen.process-datatype ×2 (stdlib) 34 ms the fasl "dt" record re-ran the shen.<datatype> yacc parser + type theory, because shen.*datatypes* entries hold closures
shen.*lambdatable* rebuild ×285 19 ms one shen.lambda-entry compile per entry, each shen.assoc->'d into a 568-entry list by non-tail KL recursion
install.shen driver 23 ms reading it through the kernel reader (12 ms) + the (external stlib)/systemf/preclude-all-but block — outside any file, so no per-file cache could ever hold it

What this PR does

Five changes, each replacing a recomputation with a cache of the state it produces.

1. Kernel type signatures in the bytecode cache (SHENKC2SHENKC3). types.kl's trailing declare block is hoisted out of the concatenated chunk so boot.lua runs it, and one dumped prolog abstraction per signature goes in the cache. The variance check is a static check of the kernel's own signatures against sources the cache key already covers; the abstraction is deterministic. Only ever a contiguous trailing run is hoisted, so hoisting cannot reorder a file's effects.

2. "dv" fasl record replaces "dt". shen.*datatypes* / shen.*alldatatypes* entries are all (TypeName . (fn TypeName)) (sequent.kl shen.remember-datatype), so the record stores the names in order and rebuilds the list; the datatype's own work then replays from its recorded chunks. Whole-list rather than a delta, so (preclude-all-but []) shrinking the table replays correctly.

3. Native shen.lambda-entry and shen.assoc->. shen.lambda-entry compiled a Lua chunk to produce a curried chain over a late-bound F lookup and nothing else — built directly instead. It is installed early (its own installer, no dependency on the compiled original) so it is in place before declarations.kl builds the lambda table; reaching that point is what generalises hoist_declares into hoist_tail, which puts a file's trailing non-defun run into a separate ":init" chunk. shen.assoc-> was non-tail KL recursion over lists hundreds of entries long — native iterative prefix copy.

4. Standard-library boot image. The whole stdlib phase — install.shen's own forms and the ~20 nested loads alike — is recorded as one fasl record stream and replayed in one go: the nearest a Lua host gets to shen-cl's save-lisp-and-die, which is the actual reason shen-cl starts in 0.01 s. Nested loads splice their record streams into the enclosing recording; a fasl replay is marked in-chunk for the enclosing span, since a replay drives the very functions the recorder wraps.

Invalidation and semantics

The image is keyed on the kernel key + record format + Prolog engine + the on-disk install.shen, and it stores the path and content hash of every file the recorded span loaded, all re-verified on each boot. The per-file fasl caches are still written on a miss, so an image that cannot be serialized costs nothing beyond not existing. FASL_ROLL is folded from the same contents in the same order, so a user program's fasl key is identical either way. SHEN_STDLIB_IMAGE=off disables it; a single-file bundle opts out (it materialises the stdlib to a fresh temp dir each boot).

A cached boot is indistinguishable from an uncached one at the Shen level: same shen.*sigf* contents and order, same lambda table, same datatypes, same shen.*gensym* and (inferences), same typechecking. shen.*gensym* is lower than main's by the ~1300 names the skipped abstractions used to consume — the counter only grows within a session, so no name it can still produce was ever handed out, and cached/uncached agree exactly, which is the invariant that matters.

Results

Interleaved A/B against main @ 67e2f43, min-of-N child CPU. This box has heavy, bursty background load (load average 13-14 during these runs), which inflates every absolute number by roughly 2×; the ratios are what hold. Reference quiet-machine numbers for main reproduce issue #46's 0.154 s.

bin/shen -e '(output "hi~%")', warm, min of 40 interleaved:

CPU vs shen-cl
shen-cl script 0.029 s
shen-lua main 0.352 s 12.3×
shen-lua this PR 0.143 s 5.0×

In-process phase split, min of 25 interleaved:

phase main this PR
load_kernel 0.0985 0.0394
initialise (stdlib) 0.2143 0.0746
total 0.3168 0.1185

Cold start (rm both caches first, min of 5) is unchanged: 1.89 s → 1.93 s CPU — the cold path does the same work plus writing the image.

Gates

  • luajit run-kernel-tests.lua134 passed / 0 failed, ok
  • make test896 pass / 0 fail across 19 specs (19 new)
  • luajit examples/{openresty,openresty-authz,envoy}/selftest.lua — all OK
  • bifrost python3 bifrost.py --impls shen-lua31 pass, 0 diverge, 0 FAIL
  • build/make-bundle.lua rebuilt and exercised (kernel cache format changed)
  • all gates run from a fully cold cache state

New test/boot_cache_spec.lua (19 assertions) pins cached-boot == uncached-boot across every configuration, asserts the image is genuinely hit (not vacuously skipped), asserts editing a standard-library file invalidates it (against a copy via SHEN_STDLIB_DIR, so the checkout is never mutated), and differential-tests both natives against the compiled-KL definitions compiled fresh out of klambda/ under an alias.

Reuben Brooks and others added 4 commits August 11, 2026 14:37
Warm start-up was 0.16 s of which nothing was compilation: the kernel
bytecode cache and the user fasl cache were both hitting, and the time
went entirely on state that the caches had no way to express and so
rebuilt from scratch on every single start. Four such items, each
replaced by a cache of the state it produces:

* klambda/types.kl's trailing 161 `(declare Name Type)` forms. `declare`
  runs the type theory for real on each one — shen.variancy under the
  Prolog machine plus (eval-kl (shen.prolog-abstraction Type)), a full
  KL->Lua compile and loadstring per signature — ~43 ms, the largest
  single item in a warm boot. The variance check is a static check of the
  kernel's own signatures against sources the cache key already covers,
  and the abstraction is deterministic, so hoist the block out of the
  concatenated chunk (only ever a contiguous TRAILING run, so no
  reordering is possible) and cache one dumped abstraction per signature.
  Cache format SHENKC2 -> SHENKC3, which also carries the gensym and
  inference counters `declare` would have advanced, so a cached boot is
  indistinguishable from an uncached one at the Shen level.

* shen.process-datatype, re-executed by the fasl "dt" record because
  shen.*datatypes* entries hold unserializable closures. They are all
  (TypeName . (fn TypeName)) (sequent.kl shen.remember-datatype), so the
  new "dv" record stores the NAMES in order and rebuilds the list —
  whole-list, not a delta, so (preclude-all-but ...) shrinking the table
  replays correctly. The datatype's own work then replays from its
  recorded chunks: ~17 ms per datatype, 2 in the standard library.

* shen.lambda-entry ran the whole compiler to produce a curried chain over
  a late-bound F lookup and nothing else — one chunk compile per lambda
  table entry, i.e. per `define` and per name a fasl replay rebuilds (285
  in a warm stdlib load). Build the chain directly; the innermost call
  goes through APP on the name symbol, matching the codegen for a name
  whose arity is unknown at compile time.

* shen.assoc->, the update primitive behind shen.*lambdatable*,
  shen.*sigf* and shen.*datatypes*, was the KL non-tail recursion: a
  stack frame and an F-table lookup per entry scanned, over lists several
  hundred entries long. Native iterative prefix copy.

Semantics: cold and warm boots produce byte-identical shen.*sigf*
(contents and order), shen.*lambdatable*, shen.*alldatatypes*,
shen.*gensym* and (inferences), and both match main. Differential-tested
shen.assoc-> and shen.lambda-entry against the compiled-KL originals.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Everything in the previous commit caches a PIECE of the boot. This caches
the standard library PHASE — the closest a Lua host gets to shen-cl's
save-lisp-and-die, which is why shen-cl starts in 0.01 s: it does not
rebuild anything.

A warm stdlib load was ~20 separate fasl hits plus the install.shen
driver around them, and the driver is not free: reading install.shen
through the kernel reader alone is ~12 ms, and the trailing
(external stlib) / systemf / (preclude-all-but []) block another ~11 ms.
No per-file cache can ever capture that, because it does not happen
inside a file.

The image records the ENTIRE span — driver forms and nested loads alike,
in stream order — as one fasl record stream and replays it in one go. The
nested loads splice their own streams into the enclosing recording
(splice_into_outer), so a load that was itself a fasl hit still
contributes its records; a fasl REPLAY is marked in-chunk for the
enclosing span, since a replay drives the very functions the recorder
wraps and would otherwise be captured a second time and captured wrong
(it reinstates chunks through load_chunk, so it contributes no "c"
records). A top-level (load "X") in install.shen is dispatched straight
to `load` instead of through `eval` — what it compiles to anyway — so the
image cannot contain a chunk that re-runs the load it replaced.

Invalidation is by construction: the file name keys on the kernel key,
the record format, the Prolog engine and the on-disk install.shen text,
and the image itself carries the path and content hash of EVERY file the
recorded span loaded, all of which are re-verified on each boot. The
per-file fasl caches are still written on a miss, so an image that cannot
be serialized costs nothing beyond not existing. FASL_ROLL is folded from
the same contents in the same order, so a user program's fasl key is
identical whether the stdlib came from the image or from its 20 files.

SHEN_STDLIB_IMAGE=off disables it. A single-file bundle materialises the
stdlib to a fresh temp directory every boot, so it opts out rather than
leave a megabyte behind per run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
declarations.kl ends with (shen.build-lambda-table (external shen)),
which maps shen.lambda-entry over ~280 external symbols — and
shen.lambda-entry ran a full KL->Lua compile and loadstring per name.
That one form was two thirds of what remained of a cached kernel load
after the type-signature block was cached.

The native shen.lambda-entry added in the previous commit could not help
it: install_native_stdlib runs after the whole kernel is loaded, long
after declarations.kl. So it moves to its own installer with no
dependency on the compiled original (`arity` returns an integer or -1 for
every input, so every branch is reachable natively), and boot.lua
installs it at the point where declarations.kl has defined `arity` and is
about to build the table.

Reaching that point means the form has to be outside the file's
concatenated chunk, so hoist_declares generalises to hoist_tail: a kernel
file's trailing run of non-defun top-level forms becomes a separate
":init" chunk, cached like any other, run immediately after the body — so
no reordering is possible — with the trailing declare block, if any, after
it. Exactly two files in the 41.2 kernel have such a run: types.kl (the
161 signatures) and declarations.kl (this one form). Anything less tidy
than [defuns][inits][declares] is left inline and cached as before.

shen.*gensym* after boot is now lower than main's by the ~1300 names the
skipped abstractions and lambda-function terms used to consume. The
counter only ever grows within a session, so no name it can still produce
was ever handed out; cached and uncached boots agree exactly, which is
the invariant that matters.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…46)

test/boot_cache_spec.lua (19 assertions) locks in the property that makes
the whole series legitimate: a cached boot must be indistinguishable from
an uncached one at the Shen level. It boots subprocesses in every cache
configuration — nothing cached, kernel bytecode cache cold and warm,
stdlib image miss and hit, image explicitly off — and diffs a state
fingerprint covering shen.*sigf* by name AND order, the lambda table, both
datatype tables, shen.*gensym*, (inferences), and a real typecheck of a
user definition. It also asserts the image is genuinely exercised (an
image hit is observed, not assumed) and that editing a standard-library
file invalidates it, against a COPY of lib/StLib via SHEN_STDLIB_DIR so
the checkout is never mutated.

Part 2 differential-tests the two natives against the compiled-KL
definitions they replace, compiled fresh out of klambda/ under an alias:
shen.assoc-> over 11 shapes (empty, replace, append, duplicate keys,
non-pair entries, numeric/string keys, improper tail, non-list) and
shen.lambda-entry over 8 live arities including partial application and
the arity 0 / unknown cases.

README: the cache section becomes three caches, with what the bytecode
cache now additionally carries, what the boot image is and how it
invalidates, SHEN_STDLIB_IMAGE=off, and the cached-equals-uncached
guarantee the new spec enforces.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pyrex41
pyrex41 merged commit 1bdc4bf into main Aug 11, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant