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>
91 lines
3.4 KiB
C
91 lines
3.4 KiB
C
/* Native-C mandelbrot, exposed as a Janet module — the lever-1 ceiling probe for
|
|
* the foundational-runtime epic (jolt-5vsp). Measures:
|
|
* (1) mandel/run-c — whole run in C (count_point inlined in C). The pure
|
|
* native-codegen ceiling: no Janet in the hot loop.
|
|
* (2) mandel/count-point-c — just count_point exposed as a Janet cfunction, so a
|
|
* Janet `while` loop can call it n^2 times. Measures the
|
|
* Janet->C boundary-crossing cost — the incremental
|
|
* hybrid (hot fn in C, caller still bytecode) pays this.
|
|
* Build: jpm --local build (project.janet declares the native module). */
|
|
#include <janet.h>
|
|
|
|
/* Pure C. cr/ci/cap are doubles; cap compared as int iteration count. */
|
|
static long count_point(double cr, double ci, long cap) {
|
|
long i = 0;
|
|
double zr = 0.0, zi = 0.0;
|
|
while (i < cap && (zr*zr + zi*zi) <= 4.0) {
|
|
double nzr = zr*zr - zi*zi + cr;
|
|
double nzi = 2.0*zr*zi + ci;
|
|
zr = nzr; zi = nzi;
|
|
i++;
|
|
}
|
|
return i;
|
|
}
|
|
|
|
static long run_c(long n) {
|
|
long cap = 200;
|
|
double nd = (double)n;
|
|
long acc = 0;
|
|
for (long y = 0; y < n; y++) {
|
|
double ci = (2.0*y)/nd - 1.0;
|
|
long a = 0;
|
|
for (long x = 0; x < n; x++) {
|
|
double cr = (2.0*x)/nd - 1.5;
|
|
a += count_point(cr, ci, cap);
|
|
}
|
|
acc += a;
|
|
}
|
|
return acc;
|
|
}
|
|
|
|
static Janet cfun_run_c(int32_t argc, Janet *argv) {
|
|
janet_fixarity(argc, 1);
|
|
long n = (long)janet_getinteger(argv, 0);
|
|
return janet_wrap_number((double)run_c(n));
|
|
}
|
|
|
|
/* count_point exposed for the Janet-loop-calls-C boundary test. */
|
|
static Janet cfun_count_point_c(int32_t argc, Janet *argv) {
|
|
janet_fixarity(argc, 3);
|
|
double cr = janet_getnumber(argv, 0);
|
|
double ci = janet_getnumber(argv, 1);
|
|
long cap = (long)janet_getinteger(argv, 2);
|
|
return janet_wrap_number((double)count_point(cr, ci, cap));
|
|
}
|
|
|
|
/* run loop in C, but count_point is a Janet function called back via janet_call
|
|
* n^2 times — the reverse crossing: a C-compiled hot fn invoking a cold bytecode
|
|
* helper. Measures janet_call overhead (the cost the hybrid pays when native code
|
|
* calls back into the bytecode world). */
|
|
static Janet cfun_run_callback(int32_t argc, Janet *argv) {
|
|
janet_fixarity(argc, 2);
|
|
long n = (long)janet_getinteger(argv, 0);
|
|
JanetFunction *cp = janet_getfunction(argv, 1);
|
|
long cap = 200;
|
|
double nd = (double)n;
|
|
long acc = 0;
|
|
for (long y = 0; y < n; y++) {
|
|
double ci = (2.0*y)/nd - 1.0;
|
|
long a = 0;
|
|
for (long x = 0; x < n; x++) {
|
|
double cr = (2.0*x)/nd - 1.5;
|
|
Janet args[3] = { janet_wrap_number(cr), janet_wrap_number(ci),
|
|
janet_wrap_number((double)cap) };
|
|
Janet r = janet_call(cp, 3, args);
|
|
a += (long)janet_unwrap_number(r);
|
|
}
|
|
acc += a;
|
|
}
|
|
return janet_wrap_number((double)acc);
|
|
}
|
|
|
|
static const JanetReg cfuns[] = {
|
|
{"run-c", cfun_run_c, "(mandel/run-c n) whole mandelbrot run in native C."},
|
|
{"count-point-c", cfun_count_point_c, "(mandel/count-point-c cr ci cap) one point, native C."},
|
|
{"run-callback", cfun_run_callback, "(mandel/run-callback n count-point-fn) C loop calling a Janet fn back via janet_call."},
|
|
{NULL, NULL, NULL}
|
|
};
|
|
|
|
JANET_MODULE_ENTRY(JanetTable *env) {
|
|
janet_cfuns(env, "mandel", cfuns);
|
|
}
|