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.
This commit is contained in:
parent
3c853429f9
commit
0087763dc9
4 changed files with 216 additions and 0 deletions
102
spike/chez/RESULTS.md
Normal file
102
spike/chez/RESULTS.md
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
# 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
|
||||
25
spike/chez/fib.ss
Normal file
25
spike/chez/fib.ss
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
;; fib spike — translated from bench/fib.clj. Pure call + integer arith.
|
||||
;; chez --script fib.ss [n=30] [optlevel=2]
|
||||
(import (chezscheme))
|
||||
(optimize-level
|
||||
(let ((a (command-line-arguments)))
|
||||
(if (and (pair? a) (pair? (cdr a))) (string->number (cadr a)) 2)))
|
||||
|
||||
(define (fib n) (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2)))))
|
||||
(define (run n) (fib n))
|
||||
|
||||
(define (now-ns)
|
||||
(let ((t (current-time 'time-monotonic)))
|
||||
(+ (* (time-second t) 1000000000) (time-nanosecond t))))
|
||||
|
||||
(let* ((a (command-line-arguments))
|
||||
(n (if (pair? a) (string->number (car a)) 30)))
|
||||
(run (- n 6)) (run (- n 6)) ; warmup
|
||||
(let loop ((k 0) (acc '()))
|
||||
(if (< k 3)
|
||||
(let* ((t0 (now-ns)) (r (run n)) (ms (/ (- (now-ns) t0) 1000000.0)))
|
||||
(loop (+ k 1) (cons ms acc)))
|
||||
(begin
|
||||
(printf "fib n ~a result ~a\n" n (run n))
|
||||
(printf "runs: ~a\n" (reverse acc))
|
||||
(printf "mean: ~a ms\n" (exact->inexact (/ (apply + acc) 3.0)))))))
|
||||
45
spike/chez/mandelbrot-fl.ss
Normal file
45
spike/chez/mandelbrot-fl.ss
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
;; mandelbrot, flonum-specialized — what a type-aware jolt->Chez backend would
|
||||
;; emit (fl*/fl+/fl< unbox; fx ops for the integer counter). This is the real
|
||||
;; substrate ceiling vs the generic version (which boxes every flonum).
|
||||
;; chez --script mandelbrot-fl.ss [n=200] [optlevel=3]
|
||||
(import (chezscheme))
|
||||
(optimize-level
|
||||
(let ((a (command-line-arguments)))
|
||||
(if (and (pair? a) (pair? (cdr a))) (string->number (cadr a)) 3)))
|
||||
|
||||
(define (count-point cr ci cap)
|
||||
(let loop ((i 0) (zr 0.0) (zi 0.0))
|
||||
(if (or (fx>= i cap) (fl> (fl+ (fl* zr zr) (fl* zi zi)) 4.0))
|
||||
i
|
||||
(loop (fx+ i 1)
|
||||
(fl+ (fl- (fl* zr zr) (fl* zi zi)) cr)
|
||||
(fl+ (fl* 2.0 (fl* zr zi)) ci)))))
|
||||
|
||||
(define (run n)
|
||||
(let ((cap 200) (nd (fixnum->flonum n)))
|
||||
(let loopy ((y 0) (acc 0))
|
||||
(if (fx< y n)
|
||||
(let* ((ci (fl- (fl/ (fl* 2.0 (fixnum->flonum y)) nd) 1.0))
|
||||
(row (let loopx ((x 0) (a 0))
|
||||
(if (fx< x n)
|
||||
(let ((cr (fl- (fl/ (fl* 2.0 (fixnum->flonum x)) nd) 1.5)))
|
||||
(loopx (fx+ x 1) (fx+ a (count-point cr ci cap))))
|
||||
a))))
|
||||
(loopy (fx+ y 1) (fx+ acc row)))
|
||||
acc))))
|
||||
|
||||
(define (now-ns)
|
||||
(let ((t (current-time 'time-monotonic)))
|
||||
(+ (* (time-second t) 1000000000) (time-nanosecond t))))
|
||||
|
||||
(let* ((a (command-line-arguments))
|
||||
(n (if (pair? a) (string->number (car a)) 200)))
|
||||
(run n) (run n)
|
||||
(let loop ((k 0) (acc '()))
|
||||
(if (< k 3)
|
||||
(let* ((t0 (now-ns)) (r (run n)) (ms (/ (- (now-ns) t0) 1000000.0)))
|
||||
(loop (+ k 1) (cons ms acc)))
|
||||
(begin
|
||||
(printf "mandelbrot-fl n ~a result ~a\n" n (run n))
|
||||
(printf "runs: ~a\n" (reverse acc))
|
||||
(printf "mean: ~a ms\n" (exact->inexact (/ (apply + acc) 3.0)))))))
|
||||
44
spike/chez/mandelbrot.ss
Normal file
44
spike/chez/mandelbrot.ss
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
;; mandelbrot spike — translated from bench/mandelbrot.clj. Pure float compute,
|
||||
;; tight recur loops (here named-let tail loops). cap=200 like the .clj.
|
||||
;; chez --script mandelbrot.ss [n=200] [optlevel=2]
|
||||
(import (chezscheme))
|
||||
(optimize-level
|
||||
(let ((a (command-line-arguments)))
|
||||
(if (and (pair? a) (pair? (cdr a))) (string->number (cadr a)) 2)))
|
||||
|
||||
(define (count-point cr ci cap)
|
||||
(let loop ((i 0) (zr 0.0) (zi 0.0))
|
||||
(if (or (>= i cap) (> (+ (* zr zr) (* zi zi)) 4.0))
|
||||
i
|
||||
(loop (+ i 1)
|
||||
(+ (- (* zr zr) (* zi zi)) cr)
|
||||
(+ (* 2.0 (* zr zi)) ci)))))
|
||||
|
||||
(define (run n)
|
||||
(let ((cap 200) (nd (* 1.0 n)))
|
||||
(let loopy ((y 0) (acc 0))
|
||||
(if (< y n)
|
||||
(let* ((ci (- (/ (* 2.0 y) nd) 1.0))
|
||||
(row (let loopx ((x 0) (a 0))
|
||||
(if (< x n)
|
||||
(let ((cr (- (/ (* 2.0 x) nd) 1.5)))
|
||||
(loopx (+ x 1) (+ a (count-point cr ci cap))))
|
||||
a))))
|
||||
(loopy (+ y 1) (+ acc row)))
|
||||
acc))))
|
||||
|
||||
(define (now-ns)
|
||||
(let ((t (current-time 'time-monotonic)))
|
||||
(+ (* (time-second t) 1000000000) (time-nanosecond t))))
|
||||
|
||||
(let* ((a (command-line-arguments))
|
||||
(n (if (pair? a) (string->number (car a)) 200)))
|
||||
(run n) (run n) ; warmup
|
||||
(let loop ((k 0) (acc '()))
|
||||
(if (< k 3)
|
||||
(let* ((t0 (now-ns)) (r (run n)) (ms (/ (- (now-ns) t0) 1000000.0)))
|
||||
(loop (+ k 1) (cons ms acc)))
|
||||
(begin
|
||||
(printf "mandelbrot n ~a result ~a\n" n (run n))
|
||||
(printf "runs: ~a\n" (reverse acc))
|
||||
(printf "mean: ~a ms\n" (exact->inexact (/ (apply + acc) 3.0)))))))
|
||||
Loading…
Add table
Add a link
Reference in a new issue