diff --git a/.dev/debt.yaml b/.dev/debt.yaml index a68df2247..d8026fd08 100644 --- a/.dev/debt.yaml +++ b/.dev/debt.yaml @@ -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: |- @@ -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: |- diff --git a/bench/results/size_history.yaml b/bench/results/size_history.yaml index dd92bc35e..77926ae98 100644 --- a/bench/results/size_history.yaml +++ b/bench/results/size_history.yaml @@ -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 diff --git a/src/api/jit_host_bridge.zig b/src/api/jit_host_bridge.zig index 221e82574..d6b50e15d 100644 --- a/src/api/jit_host_bridge.zig +++ b/src/api/jit_host_bridge.zig @@ -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; }