jolt/spike/native/bench-native.janet
Dmitri Sotnikov a2ce6bb5f6
Spike: native codegen (lever 1) feasibility for jolt-5vsp (#144)
Probes the ceiling and incremental strategy for compiling hot fns to native C,
the only lever that moves the ~10.8x Janet-VM floor the localization spike found.

Native-C mandelbrot (Janet native module) runs ~10-12ms — faster than JVM
Clojure (14.2ms) and ~18-22x faster than jolt's 219ms. The boundary cost is
asymmetric: a bytecode loop calling a C hot-fn 40k times is nearly free (~11ms),
but a C fn calling back into bytecode via janet_call costs ~3.5us/call (~152ms,
no win). So the strategy is leaf-first / whole-hot-cluster compilation, crossing
only at cold edges. A plain cc-built .so (no jpm) loads at runtime via require at
full speed, so the native tier fits jolt's dynamic compile model.

Adds the spike artifacts under spike/native/ and the writeup. Next step is
jolt-ihdp (IR->C for the numeric subset). No source changes.

Co-authored-by: Yogthos <yogthos@gmail.com>
2026-06-16 16:30:17 +00:00

53 lines
1.7 KiB
Text

# Benchmark the native-C mandelbrot vs the spike's other legs (jolt-5vsp lever 1).
# janet spike/native/bench-native.janet 200
(import ./build/mandel :as mandel)
(defn bench [label f n]
(repeat 2 (f (div n 2)))
(def times @[])
(var last-r 0)
(repeat 3
(def t0 (os/clock))
(set last-r (f n))
(array/push times (* 1000.0 (- (os/clock) t0))))
(printf "%-28s n %d result %d mean %.2f ms"
label n last-r (/ (sum times) 3)))
# Leg A: whole run in native C (pure native-codegen ceiling).
(defn run-pure-c [n] (mandel/run-c n))
# Leg B: Janet `while` loop, but count-point is a native C cfunction called n^2
# times — measures the Janet->C boundary-crossing cost (the incremental hybrid).
(defn run-boundary [n]
(def cap 200)
(def nd (* 1.0 n))
(var acc 0)
(var y 0)
(while (< y n)
(def ci (- (/ (* 2.0 y) nd) 1.0))
(var x 0)
(var a 0)
(while (< x n)
(def cr (- (/ (* 2.0 x) nd) 1.5))
(set a (+ a (mandel/count-point-c cr ci cap)))
(++ x))
(set acc (+ acc a))
(++ y))
acc)
# Leg C: C run loop calling a Janet bytecode count-point back via janet_call n^2
# times — the reverse crossing (hot C fn -> cold bytecode helper).
(defn count-point-janet [cr ci cap]
(var i 0) (var zr 0.0) (var zi 0.0)
(while (and (< i cap) (<= (+ (* zr zr) (* zi zi)) 4.0))
(def nzr (+ (- (* zr zr) (* zi zi)) cr))
(def nzi (+ (* 2.0 (* zr zi)) ci))
(set zr nzr) (set zi nzi) (++ i))
i)
(defn run-callback [n] (mandel/run-callback n count-point-janet))
(defn main [& args]
(def n (if (> (length args) 1) (scan-number (get args 1)) 200))
(bench "native-C whole run" run-pure-c n)
(bench "Janet loop -> C count-point" run-boundary n)
(bench "C loop -> janet_call back" run-callback n))