jolt/spike/chez/RESULTS.md
Yogthos 0087763dc9 Chez re-host spike: substrate ceiling vs Janet (fib/mandelbrot)
Hand-translate the two compute benches into the Scheme a jolt->Chez backend
would emit, to localize the execution-substrate ceiling without porting the RT.

fib 30: 246.6 -> 5.2 ms (~47x, fixnum). mandelbrot 200: 166.3 -> 13.4 ms
(~12.4x) ONLY with flonum-specialized ops; generic float ops box every flonum
and stay ~1.7x. 13.4 ms matches jolt's JOLT_CGEN C result, so Chez's native
compiler reaches the C ceiling with no cc step, REPL intact.

Size: Chez base 2.9 MB (AOT) / 4.0 MB (dynamic) vs Janet 2.21. Memory: Chez
~32-49 MB fixed baseline vs Janet ~12 MB (the one regression). RT-bound axes
(collections/binary-trees, where Chez's generational GC should help) not yet
measured. See spike/chez/RESULTS.md.
2026-06-17 12:07:35 -04:00

5.5 KiB

Chez Scheme re-host spike — results

Branch spike/chez-bootstrap. Question: would re-hosting jolt's substrate from Janet onto Chez Scheme (cisco/ChezScheme 10.4.1) buy speed, at what size/memory cost? This spike does NOT port jolt-core/RT — it measures the execution substrate ceiling by hand-translating the two compute-bound benches (fib, mandelbrot) into the Scheme a jolt->Chez backend would emit, plus real size/memory of the Chez runtime.

Machine: darwin arm64, M-series. Same caveat as the handoff doc — this dev box swaps under load, so alloc-heavy absolute numbers inflate; compute benches (fib/mandelbrot) are trustworthy. All runs isolated (no other CPU work).

Speed (mean ms, 3 runs after warmup; same sizes as bench/run.sh)

Bench Janet jolt Chez best Speedup Note
fib 30 246.6 5.2 ~47x fixnum arith — immediate, unboxed
mandelbrot 200 (generic ops) 166.3 98.1 ~1.7x + - * > box every flonum
mandelbrot 200 (flonum ops) 166.3 13.4 ~12.4x fl*/fl+/fl< unboxed

Correctness verified: fib 30 = 832040, mandelbrot 200 count = 3288753 (both match jolt). optimize-level 2 vs 3 made no material difference here.

The key finding is the mandelbrot split. Generic Scheme arithmetic on floats sends Chez through the numeric tower and heap-boxes every flonum — so the naive emit gets almost nothing (~1.7x) and opt-level doesn't help. Emitting flonum-specific ops (fl+/fl*/fl<, fx for the integer counter) lets Chez keep flonums unboxed in registers and the same code drops to 13.4 ms.

13.4 ms ~= jolt's own JOLT_CGEN C-codegen result (12.4 ms, which already beat JVM per docs/foundational-runtime-lever1-native-codegen.md). So Chez's native compiler reaches the hand-emitted-C ceiling on its own, with no separate cc step, no .so cache, no AOT manifest — just runtime compilation, REPL intact.

Implication for a real backend: the win is gated on the same type-inference -> specialized-op lowering jolt ALREADY has (passes/types.clj feeds native-arith on Janet today). fib's 47x is free (fixnums); mandelbrot's 12x needs that typed path wired to fl* emission instead of (or alongside) the Janet/C path.

Size (deployable footprint)

App code is negligible — fib compiled to a native object (compile-program, optimize-level 3) is 2 KB. The footprint is the Chez runtime:

Artifact Size vs Janet
Janet build/jolt (complete, jolt baked in) 2.21 MB 1.0x
Chez base, AOT (kernel + petite.boot + app) 2.89 MB 1.3x
Chez base, dynamic/REPL (+ scheme.boot compiler) 3.96 MB 1.8x

components: libkernel.a 0.83 MB, petite.boot (runtime lib) 2.07 MB, scheme.boot (compiler) 1.07 MB.

Caveat: the Chez rows are the runtime base ONLY. A complete jolt adds compiled jolt-core (analyzer + clojure.core + persistent-collection RT) on top, which the Janet 2.21 MB already includes. Estimated full Chez jolt ~4-6 MB. Still single-digit MB, ~2-3x Janet, vastly under a JVM (40 MB+). petite.boot carries much jolt won't use; a stripped custom boot file could shrink it.

Memory (max RSS)

Scenario Janet Chez
startup / trivial 12.5 MB 32.1 (petite) / 49.5 (full)
mandelbrot 200 20.8 MB ~32 MB (AOT under petite)
fib 30 19.8 MB 32.1 MB

Chez's baseline is flat across workloads (fib allocates ~nothing and doesn't move it), so the ~32 MB (runtime) / ~49.5 MB (runtime + resident compiler) is fixed reservation, not workload allocation. This is the one axis where Chez is clearly worse: ~2.5x Janet's fixed footprint. Trades RAM for speed. (Potentially tunable via Chez heap params / a stripped boot file; not explored.)

Verdict

  • Speed: validated and large on compute — 47x (fib) and 12.4x (mandelbrot), the latter matching jolt's C-codegen ceiling, conditional on the backend emitting typed/specialized numeric ops. Naive generic emit is nearly flat on floats. jolt's existing type passes are the lever that makes this real.
  • Chez could subsume the cgen path: runtime native compile gets C-level numeric speed while keeping live redefinition — collapsing the interpret/compile/cgen-to-C hybrid into one native path.
  • Size: fine (~1.3-1.8x base, ~2-3x full; single-digit MB).
  • Memory: the cost (~2.5x fixed baseline).

NOT yet measured (needs the RT port — the real project, not a spike)

  • collections / binary-trees: these hit persistent collections + GC. Chez's GC is generational (vs Janet's non-generational mark-sweep), so binary-trees (jolt's worst axis, ~314x JVM) is exactly where Chez's GC should help most — but it requires porting the persistent-collection RT first. This is the next validation and the highest-uncertainty remaining question.
  • Startup time (Janet jolt baked-image ~20ms; Chez boot-file load TBD).
  • fiber/async layer (Janet fibers -> call/cc + threads rebuild).

Repro

cd spike/chez
chez --script fib.ss 30 3
chez --script mandelbrot.ss 200 3        # generic (boxed) — slow
chez --script mandelbrot-fl.ss 200 3     # flonum-typed — the ceiling