Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 25 additions & 17 deletions .dev/debt.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,19 @@ entries:
description: |-
Binary-size campaign lever #2 (ADR-0204 D2): complete the table-driven
arm64 dispatch and DELETE the legacy switch. `engine.codegen.arm64.emit
.compile` is a single 707 KB __text symbol (~161-arm switch over ZirOp,
arms mostly delegate to already-extracted op_*.zig handlers); the
dispatch_collector substrate (ADR-0074) already table-dispatches
registered ops and falls through to the switch. Finish registration for
all remaining ops, delete the fall-through. Same treatment for the
x86_64 twin (costs Linux/Windows binaries; already target-gated out of
the arm64 binary). Acceptance: emitted JIT bytes IDENTICAL —
test-aot-diff 63/63 + fuzz-diff lanes + 3-OS test-all.
.compile` is a single 707 KB __text symbol. Root cause sharpened
2026-07-16: `dispatch_collector.dispatch()` is an `inline for` +
`@call(.auto)` over the 409 registered arm64 op modules — every
registered handler body is INLINED into the compile() instantiation
(and dispatch is a linear O(n) compare chain in the JIT-compile hot
path), on top of the ~161 legacy switch arms inlining their op_*.zig
helpers. Stage A = convert dispatch()/dispatchX86_64Ctx() to a
comptime-built fn-pointer table indexed by @intFromEnum(op) (O(1)
dispatch, kills the inline duplication; handlers need a uniform
canonical signature per axis — wrap where inferred error sets
differ). Stage B = migrate the remaining ~161 switch ops to per-op
files and delete the switch. Acceptance: emitted JIT bytes
IDENTICAL — test-aot-diff 63/63 + fuzz-diff lanes + 3-OS test-all.
first_raised: "2026-07-16"
last_reviewed: "2026-07-16"
refs: |-
Expand All @@ -32,15 +37,18 @@ entries:
t1fp × 960 = 282 KB, t0..t4 × 320 each ≈ 124 KB; measured 2026-07-16,
ReleaseSafe arm64 CLI 5,282,584 B at 71cccba76). Axes characterized
2026-07-16: arg-kinds x RetKind(5) x MAX_HOST_SLOTS(64); the x64
slot axis (94% of the product) exists only to comptime-hardcode the
slot index K. Stage 1 = shared FP-bridge delegation (t1fp/t2fp
currently inline ~300 B of payload-lookup+invokeCb per thunk vs the
GP thunks' 88 B bridge() delegation) — no ABI/JIT change, ~0.8 MB.
Stage 2 = collapse the slot axis by conveying K at runtime (call
site materializes slot index / payload ptr into a scratch reg
before BLR) — changes zwasm's own emitted call sequence (internal,
AOT-cache version-keyed), ~0.4 MB more. Constraints: no api break,
no per-call regression beyond noise (ADR-0181 bench series).
slot axis exists only to comptime-hardcode the slot index K.
**Stage 1 DONE 2026-07-16** (shared noinline fpBridge1/fpBridge2
bodies; thunks forward-only): jit_host_bridge 1,311 KB -> 232 KB
(-82%), whole CLI 5,282,584 -> 4,173,736 B (-21%), no ABI change.
Stage 2 (collapse the slot axis by conveying K at runtime — call
site materializes the slot index before BLR) is RE-SCORED by the
stage-1 measurement: remaining recoverable ~200 KB (209 KB of
per-slot thunks -> ~15 KB shared), against a JIT call-sequence
change on both arches + AOT format touch. Demand-driven — re-score
after D-521 lands; do not grind speculatively (measure-first).
Constraints unchanged: no api break, no per-call regression beyond
noise (ADR-0181 bench series).
first_raised: "2026-07-16"
last_reviewed: "2026-07-16"
refs: |-
Expand Down
12 changes: 12 additions & 0 deletions bench/results/size_history.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,15 @@
variant: "lean"
optimize: "ReleaseFast"
zwasm_bytes: 3646232

- commit: "698eeff5d"
timestamp: "2026-07-16T03:46:03Z"
variant: "base"
optimize: "ReleaseFast"
zwasm_bytes: 3563960

- commit: "698eeff5d"
timestamp: "2026-07-16T03:46:03Z"
variant: "lean"
optimize: "ReleaseFast"
zwasm_bytes: 3325304
31 changes: 21 additions & 10 deletions src/api/jit_host_bridge.zig
Original file line number Diff line number Diff line change
Expand Up @@ -216,25 +216,36 @@ fn t4(comptime K: usize, comptime r: RetKind) *const fn (*JitRuntime, u64, u64,
// register-class `Kind` so the C ABI lands GP args in integer registers and
// f32/f64 in FP registers (correct on Win64 too, where the slot index couples
// by position). The bridge marshals each arg via `argVal`.
//
// The bodies live in `fpBridge1`/`fpBridge2`, monomorphized per (kinds, ret)
// but NOT per slot K — `noinline` keeps them from being re-inlined into each
// of the MAX_HOST_SLOTS thunks (the D-522 measurement: inlined bodies cost
// ~300 B x 3,840 thunks; shared bodies leave only arg-forwarding per thunk).
noinline fn fpBridge1(comptime k0: Kind, comptime r: RetKind, rt: *JitRuntime, idx: usize, a0: PT(k0)) RT(r) {
const base = rt.host_payloads_base orelse return trapResult(rt, r);
const payload: *HostFuncPayload = @ptrFromInt(base[idx]);
if (payload.params.len != 1) return trapResult(rt, r);
const argbuf = [_]Val{argVal(k0, payload.params[0], a0)};
return invokeCb(rt, payload, &argbuf, r);
}
noinline fn fpBridge2(comptime k0: Kind, comptime k1: Kind, comptime r: RetKind, rt: *JitRuntime, idx: usize, a0: PT(k0), a1: PT(k1)) RT(r) {
const base = rt.host_payloads_base orelse return trapResult(rt, r);
const payload: *HostFuncPayload = @ptrFromInt(base[idx]);
if (payload.params.len != 2) return trapResult(rt, r);
const argbuf = [_]Val{ argVal(k0, payload.params[0], a0), argVal(k1, payload.params[1], a1) };
return invokeCb(rt, payload, &argbuf, r);
}
fn t1fp(comptime k0: Kind, comptime K: usize, comptime r: RetKind) *const fn (*JitRuntime, PT(k0)) callconv(.c) RT(r) {
return &struct {
fn f(rt: *JitRuntime, a0: PT(k0)) callconv(.c) RT(r) {
const base = rt.host_payloads_base orelse return trapResult(rt, r);
const payload: *HostFuncPayload = @ptrFromInt(base[K]);
if (payload.params.len != 1) return trapResult(rt, r);
const argbuf = [_]Val{argVal(k0, payload.params[0], a0)};
return invokeCb(rt, payload, &argbuf, r);
return fpBridge1(k0, r, rt, K, a0);
}
}.f;
}
fn t2fp(comptime k0: Kind, comptime k1: Kind, comptime K: usize, comptime r: RetKind) *const fn (*JitRuntime, PT(k0), PT(k1)) callconv(.c) RT(r) {
return &struct {
fn f(rt: *JitRuntime, a0: PT(k0), a1: PT(k1)) callconv(.c) RT(r) {
const base = rt.host_payloads_base orelse return trapResult(rt, r);
const payload: *HostFuncPayload = @ptrFromInt(base[K]);
if (payload.params.len != 2) return trapResult(rt, r);
const argbuf = [_]Val{ argVal(k0, payload.params[0], a0), argVal(k1, payload.params[1], a1) };
return invokeCb(rt, payload, &argbuf, r);
return fpBridge2(k0, k1, r, rt, K, a0, a1);
}
}.f;
}
Expand Down