From 3fa05edd2fe3026c61fbcea20ce182f8c91f5a5b Mon Sep 17 00:00:00 2001 From: theMackabu Date: Tue, 28 Apr 2026 11:23:33 -0700 Subject: [PATCH] add a conservative inline-feasibility pass --- src/silver/swarm.c | 193 ++++++++++++++++++++++++++- tests/bench_jit_method_inlining.js | 178 +++++++++++++++++++++++++ tests/jit_method_inlining_fuzz.js | 86 ++++++++++++ tests/jit_method_inlining_stress.js | 195 ++++++++++++++++++++++++++++ 4 files changed, 648 insertions(+), 4 deletions(-) create mode 100644 tests/bench_jit_method_inlining.js create mode 100644 tests/jit_method_inlining_fuzz.js create mode 100644 tests/jit_method_inlining_stress.js diff --git a/src/silver/swarm.c b/src/silver/swarm.c index bac20d0..fbf2647 100644 --- a/src/silver/swarm.c +++ b/src/silver/swarm.c @@ -1558,6 +1558,20 @@ static MIR_label_t inl_label_lookup(inl_label_map_t *lm, int bc_off, int *out_sp } static bool jit_inline_body_feasible(sv_func_t *callee) { + if (!callee) return false; + int max_stack = callee->max_stack > 0 ? callee->max_stack : 4; + int n_locals = callee->max_locals > 0 ? callee->max_locals : 1; + uint8_t stack_types[max_stack]; + uint8_t local_types[n_locals]; + memset(stack_types, SLOT_BOXED, sizeof(stack_types)); + memset(local_types, SLOT_BOXED, sizeof(local_types)); + int sp = 0; + bool side_effect_seen = false; + +#define FEAS_POP(n) do { if (sp < (n)) return false; sp -= (n); } while (0) +#define FEAS_PUSH(t) do { if (sp >= max_stack) return false; stack_types[sp++] = (t); } while (0) +#define FEAS_GUARD_AFTER_EFFECT() do { if (side_effect_seen) return false; } while (0) + uint8_t *ip = callee->code; uint8_t *end = callee->code + callee->code_len; while (ip < end) { @@ -1565,10 +1579,180 @@ static bool jit_inline_body_feasible(sv_func_t *callee) { int sz = sv_op_size[op]; if (sz == 0) return false; switch (op) { - default: break; + case OP_GET_ARG: + FEAS_PUSH(SLOT_BOXED); + break; + case OP_CONST_I8: + FEAS_PUSH(SLOT_NUM); + break; + case OP_CONST: { + uint32_t idx = sv_get_u32(ip + 1); + if (idx >= (uint32_t)callee->const_count) return false; + FEAS_PUSH(vtype(callee->constants[idx]) == T_NUM ? SLOT_NUM : SLOT_BOXED); + break; + } + case OP_CONST8: { + uint8_t idx = sv_get_u8(ip + 1); + if (idx >= (uint8_t)callee->const_count) return false; + FEAS_PUSH(vtype(callee->constants[idx]) == T_NUM ? SLOT_NUM : SLOT_BOXED); + break; + } + case OP_UNDEF: case OP_NULL: case OP_TRUE: case OP_FALSE: + case OP_THIS: + case OP_GET_UPVAL: + case OP_GET_GLOBAL: + case OP_SPECIAL_OBJ: + if (side_effect_seen && (op == OP_GET_GLOBAL || op == OP_SPECIAL_OBJ)) + return false; + FEAS_PUSH(SLOT_BOXED); + break; + case OP_GET_LOCAL: { + uint16_t idx = sv_get_u16(ip + 1); + if (idx >= (uint16_t)n_locals) return false; + FEAS_PUSH(local_types[idx]); + break; + } + case OP_GET_LOCAL8: { + uint8_t idx = sv_get_u8(ip + 1); + if (idx >= (uint8_t)n_locals) return false; + FEAS_PUSH(local_types[idx]); + break; + } + case OP_PUT_LOCAL: { + uint16_t idx = sv_get_u16(ip + 1); + if (idx >= (uint16_t)n_locals || sp < 1) return false; + local_types[idx] = stack_types[--sp]; + break; + } + case OP_PUT_LOCAL8: { + uint8_t idx = sv_get_u8(ip + 1); + if (idx >= (uint8_t)n_locals || sp < 1) return false; + local_types[idx] = stack_types[--sp]; + break; + } + case OP_SET_LOCAL: { + uint16_t idx = sv_get_u16(ip + 1); + if (idx >= (uint16_t)n_locals || sp < 1) return false; + local_types[idx] = stack_types[sp - 1]; + break; + } + case OP_SET_LOCAL8: { + uint8_t idx = sv_get_u8(ip + 1); + if (idx >= (uint8_t)n_locals || sp < 1) return false; + local_types[idx] = stack_types[sp - 1]; + break; + } + case OP_SET_LOCAL_UNDEF: { + uint16_t idx = sv_get_u16(ip + 1); + if (idx >= (uint16_t)n_locals) return false; + local_types[idx] = SLOT_BOXED; + break; + } + case OP_POP: + FEAS_POP(1); + break; + case OP_DUP: + if (sp < 1) return false; + { uint8_t top_type = stack_types[sp - 1]; FEAS_PUSH(top_type); } + break; + case OP_DUP2: { + if (sp < 2) return false; + uint8_t a = stack_types[sp - 2]; + uint8_t b = stack_types[sp - 1]; + FEAS_PUSH(a); + FEAS_PUSH(b); + break; + } + case OP_INSERT2: { + if (sp < 2) return false; + uint8_t a = stack_types[sp - 1]; + uint8_t obj = stack_types[sp - 2]; + stack_types[sp - 2] = a; + stack_types[sp - 1] = obj; + FEAS_PUSH(a); + break; + } + case OP_INSERT3: { + if (sp < 3) return false; + uint8_t a = stack_types[sp - 1]; + uint8_t prop = stack_types[sp - 2]; + uint8_t obj = stack_types[sp - 3]; + stack_types[sp - 3] = a; + stack_types[sp - 2] = obj; + stack_types[sp - 1] = prop; + FEAS_PUSH(a); + break; + } + case OP_ADD: case OP_SUB: case OP_MUL: case OP_DIV: + case OP_ADD_NUM: case OP_SUB_NUM: case OP_MUL_NUM: case OP_DIV_NUM: { + if (sp < 2) return false; + bool known_num = + stack_types[sp - 1] == SLOT_NUM && stack_types[sp - 2] == SLOT_NUM; + if (!known_num) FEAS_GUARD_AFTER_EFFECT(); + sp -= 2; + FEAS_PUSH(SLOT_NUM); + break; + } + case OP_MOD: + FEAS_GUARD_AFTER_EFFECT(); + FEAS_POP(2); + FEAS_PUSH(SLOT_NUM); + break; + case OP_NEG: + if (sp < 1) return false; + FEAS_GUARD_AFTER_EFFECT(); + stack_types[sp - 1] = SLOT_NUM; + break; + case OP_LT: case OP_LE: case OP_GT: case OP_GE: + FEAS_GUARD_AFTER_EFFECT(); + FEAS_POP(2); + FEAS_PUSH(SLOT_BOXED); + break; + case OP_SEQ: case OP_SNE: case OP_EQ: case OP_NE: + FEAS_POP(2); + FEAS_PUSH(SLOT_BOXED); + break; + case OP_IS_UNDEF: case OP_IS_NULL: + if (sp < 1) return false; + stack_types[sp - 1] = SLOT_BOXED; + break; + case OP_JMP: + case OP_NOP: case OP_LINE_NUM: case OP_COL_NUM: case OP_LABEL: + break; + case OP_JMP_TRUE: case OP_JMP_FALSE: + case OP_JMP_TRUE8: case OP_JMP_FALSE8: + FEAS_POP(1); + break; + case OP_JMP_TRUE_PEEK: case OP_JMP_FALSE_PEEK: + if (sp < 1) return false; + break; + case OP_RETURN: + FEAS_POP(1); + break; + case OP_RETURN_UNDEF: + break; + case OP_GET_FIELD: + if (side_effect_seen) return false; + FEAS_POP(1); + FEAS_PUSH(SLOT_BOXED); + break; + case OP_GET_FIELD2: + if (side_effect_seen) return false; + if (sp < 1) return false; + FEAS_PUSH(SLOT_BOXED); + break; + case OP_PUT_FIELD: + FEAS_POP(2); + side_effect_seen = true; + break; + default: + return false; } ip += sz; } +#undef FEAS_GUARD_AFTER_EFFECT +#undef FEAS_PUSH +#undef FEAS_POP return true; } @@ -1923,16 +2107,17 @@ static bool jit_emit_inline_body( case OP_ADD: case OP_SUB: case OP_MUL: case OP_DIV: case OP_ADD_NUM: case OP_SUB_NUM: case OP_MUL_NUM: case OP_DIV_NUM: { + bool l_known_num = inl_slot_type[isp - 2] == SLOT_NUM; + bool r_known_num = inl_slot_type[isp - 1] == SLOT_NUM; MIR_reg_t rr = inl_vs[--isp]; MIR_reg_t rl = inl_vs[--isp]; int dst_i = isp; MIR_reg_t rd = inl_vs[isp++]; - if (!(op == OP_ADD_NUM || op == OP_SUB_NUM || - op == OP_MUL_NUM || op == OP_DIV_NUM)) { + if (!l_known_num) mir_emit_is_num_guard(ctx, jit_func, r_bool, rl, slow); + if (!r_known_num) mir_emit_is_num_guard(ctx, jit_func, r_bool, rr, slow); - } if (!*p_d_slot) { *p_d_slot = MIR_new_func_reg(ctx, jit_func->u.func, diff --git a/tests/bench_jit_method_inlining.js b/tests/bench_jit_method_inlining.js new file mode 100644 index 0000000..7bf29b3 --- /dev/null +++ b/tests/bench_jit_method_inlining.js @@ -0,0 +1,178 @@ +function nowMs() { + if (typeof performance !== "undefined" && performance && typeof performance.now === "function") + return performance.now(); + return Date.now(); +} + +function parseScale() { + if (typeof process === "undefined" || !process || !process.argv) return 1; + const raw = Number(process.argv[2]); + return Number.isFinite(raw) && raw > 0 ? raw : 1; +} + +function sortNumbers(values) { + const out = values.slice(); + for (let i = 1; i < out.length; i++) { + const v = out[i]; + let j = i - 1; + while (j >= 0 && out[j] > v) { + out[j + 1] = out[j]; + j--; + } + out[j + 1] = v; + } + return out; +} + +function median(values) { + const sorted = sortNumbers(values); + return sorted[(sorted.length / 2) | 0]; +} + +const SCALE = parseScale(); +const RUNS = 7; +let sink = 0; + +function bench(label, iterations, fn) { + for (let i = 0; i < 3; i++) sink ^= fn(Math.max(1, (iterations / 10) | 0)) | 0; + + const samples = []; + let result = 0; + for (let i = 0; i < RUNS; i++) { + const t0 = nowMs(); + result = fn(iterations); + samples.push(nowMs() - t0); + } + + sink ^= result | 0; + const med = median(samples); + const opsPerMs = med > 0 ? (iterations / med).toFixed(2) : "inf"; + console.log(label + ": " + med.toFixed(3) + "ms, " + opsPerMs + " ops/ms, result=" + result); + return med; +} + +function Sequence(start) { + this.item = start; +} + +Sequence.prototype.next = function() { + const old = this.item; + this.item = old + 2; + return old; +}; + +function runInlineableMethod(n) { + const seq = new Sequence(1); + let out = 0; + for (let i = 0; i < n; i++) out = seq.next(); + return out + seq.item; +} + +function runManualEquivalent(n) { + const seq = new Sequence(1); + let out = 0; + for (let i = 0; i < n; i++) { + out = seq.item; + seq.item = out + 2; + } + return out + seq.item; +} + +function PolyA(start) { + this.item = start; +} + +function PolyB(start) { + this.padding = 1; + this.item = start; +} + +PolyA.prototype.next = Sequence.prototype.next; +PolyB.prototype.next = Sequence.prototype.next; + +function callPoly(obj) { + return obj.next(); +} + +function runPolymorphicMethod(n) { + const a = new PolyA(1); + const b = new PolyB(1); + let out = 0; + for (let i = 0; i < n; i++) out = callPoly((i & 1) === 0 ? a : b); + return out + a.item + b.item; +} + +function Vec2(x, y) { + this.x = x; + this.y = y; +} + +Vec2.prototype.sumAndBump = function(dx, dy) { + const old = this.x + this.y; + this.x = this.x + dx; + this.y = this.y + dy; + return old; +}; + +function runInlineableArgs(n) { + const v = new Vec2(1, 2); + let out = 0; + for (let i = 0; i < n; i++) out = v.sumAndBump(1, 2); + return out + v.x + v.y; +} + +function runManualArgs(n) { + const v = new Vec2(1, 2); + let out = 0; + for (let i = 0; i < n; i++) { + out = v.x + v.y; + v.x = v.x + 1; + v.y = v.y + 2; + } + return out + v.x + v.y; +} + +function LargeMethod(start) { + this.item = start; + this.flag = false; +} + +LargeMethod.prototype.next = function() { + const old = this.item; + this.item = old + 2; + if (this.flag) { + let sum = 0; + sum = sum + old; + sum = sum + this.item; + sum = sum + old; + sum = sum + this.item; + return sum; + } + return old; +}; + +function runOverBudgetMethod(n) { + const seq = new LargeMethod(1); + let out = 0; + for (let i = 0; i < n; i++) out = seq.next(); + return out + seq.item; +} + +const iterations = Math.max(100000, Math.floor(1000000 * SCALE)); + +console.log("method inlining benchmark"); +console.log("scale: " + SCALE); +console.log("iterations: " + iterations); + +const manual = bench("manual slot loop", iterations, runManualEquivalent); +const inlineable = bench("inlineable method next()", iterations, runInlineableMethod); +const poly = bench("polymorphic method fallback", iterations, runPolymorphicMethod); +const manualArgs = bench("manual arg slot loop", iterations, runManualArgs); +const inlineArgs = bench("inlineable method with args", iterations, runInlineableArgs); +const overBudget = bench("over-budget method", iterations, runOverBudgetMethod); + +console.log("next()/manual: " + (inlineable / manual).toFixed(2) + "x"); +console.log("polymorphic/inlineable: " + (poly / inlineable).toFixed(2) + "x"); +console.log("args/manual: " + (inlineArgs / manualArgs).toFixed(2) + "x"); +console.log("over-budget/inlineable: " + (overBudget / inlineable).toFixed(2) + "x"); +console.log("sink: " + sink); diff --git a/tests/jit_method_inlining_fuzz.js b/tests/jit_method_inlining_fuzz.js new file mode 100644 index 0000000..9c4ee90 --- /dev/null +++ b/tests/jit_method_inlining_fuzz.js @@ -0,0 +1,86 @@ +function assertEq(actual, expected, label) { + if (actual !== expected) + throw new Error(label + ": expected " + expected + ", got " + actual); +} + +function sharedNext(delta) { + const old = this.item; + this.item = old + delta; + return old; +} + +function runCase(k) { + function Box(start) { + if ((k & 1) === 0) { + this.item = start; + this.pad = k; + } else { + this.pad = k; + this.item = start; + } + if ((k & 2) !== 0) this.more = k + 1; + } + + Box.prototype.next = sharedNext; + + function callNext(obj, delta) { + return obj.next(delta); + } + + const obj = new Box(k + 1); + const delta = (k % 4) + 1; + for (let i = 0; i < 260; i++) callNext(obj, delta); + + const before = obj.item; + + switch (k % 5) { + case 0: { + obj.extra = k * 3; + assertEq(callNext(obj, delta), before, "expando result " + k); + assertEq(obj.item, before + delta, "expando store " + k); + assertEq(obj.extra, k * 3, "expando preserved " + k); + break; + } + case 1: { + Box.prototype.next = function(d) { + return this.item + d + 1000; + }; + assertEq(callNext(obj, delta), before + delta + 1000, "prototype replacement " + k); + assertEq(obj.item, before, "prototype replacement no old store " + k); + break; + } + case 2: { + obj.next = function(d) { + return this.item * 10 + d; + }; + assertEq(callNext(obj, delta), before * 10 + delta, "own shadow " + k); + assertEq(obj.item, before, "own shadow no old store " + k); + break; + } + case 3: { + const alt = { other: k, item: 50 + k }; + Object.setPrototypeOf(alt, Box.prototype); + assertEq(callNext(alt, delta), 50 + k, "alternate shape result " + k); + assertEq(alt.item, 50 + k + delta, "alternate shape store " + k); + assertEq(alt.other, k, "alternate shape preserved " + k); + break; + } + default: { + let backing = before + 5; + Object.defineProperty(obj, "item", { + get() { return backing; }, + set(v) { backing = v * 2; }, + configurable: true + }); + assertEq(callNext(obj, delta), before + 5, "accessor result " + k); + assertEq(backing, (before + 5 + delta) * 2, "accessor setter " + k); + break; + } + } +} + +for (let round = 0; round < 3; round++) { + for (let k = 0; k < 40; k++) runCase(round * 40 + k); +} + +console.log("jit method inlining fuzz: ok"); diff --git a/tests/jit_method_inlining_stress.js b/tests/jit_method_inlining_stress.js new file mode 100644 index 0000000..dc1f806 --- /dev/null +++ b/tests/jit_method_inlining_stress.js @@ -0,0 +1,195 @@ +function fail(label, expected, actual) { + throw new Error(label + ": expected " + expected + ", got " + actual); +} + +function assertEq(actual, expected, label) { + if (actual !== expected) fail(label, expected, actual); +} + +function warm(fn, n) { + for (let i = 0; i < (n || 300); i++) fn(i); +} + +// Basic direct-slot load/add/store through a prototype method. +function Seq(start) { + this.item = start; +} + +Seq.prototype.next = function() { + const old = this.item; + this.item = old + 2; + return old; +}; + +function runSeqLoop(n) { + const seq = new Seq(1); + let out = 0; + for (let i = 0; i < n; i++) out = seq.next(); + return out + seq.item; +} + +for (let i = 0; i < 200; i++) runSeqLoop(8); +assertEq(runSeqLoop(1000), 4000, "hot loop inline result"); + +function callSeq(seq) { + return seq.next(); +} + +const seq = new Seq(1); +warm(function() { callSeq(seq); }); +assertEq(callSeq(seq), 601, "basic inline result"); +assertEq(seq.item, 603, "basic inline store"); + +// Same call site, different receiver shape and different property slot order. +function SlotA() { + this.item = 1; +} + +SlotA.prototype.next = Seq.prototype.next; + +function callSlotA(obj) { + return obj.next(); +} + +const slotA = new SlotA(); +warm(function() { callSlotA(slotA); }); + +const slotB = { padding: 99, item: 10 }; +Object.setPrototypeOf(slotB, SlotA.prototype); +assertEq(callSlotA(slotB), 10, "receiver shape fallback result"); +assertEq(slotB.item, 12, "receiver shape fallback store"); +assertEq(slotB.padding, 99, "receiver shape fallback preserves other slot"); + +// Prototype method identity changes after the call site has warmed. +function Replaceable(start) { + this.item = start; +} + +Replaceable.prototype.next = function() { + const old = this.item; + this.item = old + 1; + return old; +}; + +function callReplaceable(obj) { + return obj.next(); +} + +const replaceable = new Replaceable(5); +warm(function() { callReplaceable(replaceable); }); +const replaceableBefore = replaceable.item; +Replaceable.prototype.next = function() { + return this.item + 1000; +}; +assertEq(callReplaceable(replaceable), replaceableBefore + 1000, "prototype method replacement"); +assertEq(replaceable.item, replaceableBefore, "prototype replacement does not run old body"); + +// Own method shadowing changes the receiver shape and callee identity. +function Shadowed(start) { + this.item = start; +} + +Shadowed.prototype.next = Seq.prototype.next; + +function callShadowed(obj) { + return obj.next(); +} + +const shadowed = new Shadowed(7); +warm(function() { callShadowed(shadowed); }); +shadowed.next = function() { + return this.item * 10; +}; +assertEq(callShadowed(shadowed), shadowed.item * 10, "own method shadow fallback"); + +// Accessor conversion after warmup must not use stale direct slot metadata. +function AccessorBox(start) { + this.item = start; +} + +AccessorBox.prototype.next = Seq.prototype.next; + +function callAccessor(obj) { + return obj.next(); +} + +const accessor = new AccessorBox(3); +warm(function() { callAccessor(accessor); }); +let backing = 20; +Object.defineProperty(accessor, "item", { + get() { return backing; }, + set(v) { backing = v + 100; }, + configurable: true +}); +assertEq(callAccessor(accessor), 20, "accessor fallback getter result"); +assertEq(backing, 122, "accessor fallback setter result"); + +// A callee with a side effect before a later shape-dependent read should not be +// inlined; otherwise a late guard failure would rerun the method and duplicate +// the side effect. +function LateGuardBox() { + this.count = 0; +} + +LateGuardBox.prototype.sideThenRead = function(other) { + this.count = this.count + 1; + return other.value; +}; + +function callLateGuard(box, other) { + return box.sideThenRead(other); +} + +const late = new LateGuardBox(); +const monoOther = { value: 1 }; +warm(function() { callLateGuard(late, monoOther); }); +const lateBefore = late.count; +const otherShape = { padding: 0, value: 7 }; +assertEq(callLateGuard(late, otherShape), 7, "late guard fallback result"); +assertEq(late.count, lateBefore + 1, "late guard does not duplicate side effect"); + +// Argument mapping and branch returns in an inlineable method body. +function Accum(start) { + this.item = start; +} + +Accum.prototype.addScaled = function(delta, scale) { + if (delta < 0) return this.item; + const old = this.item; + this.item = old + delta * scale; + return this.item; +}; + +function callAddScaled(obj, delta, scale) { + return obj.addScaled(delta, scale); +} + +const accum = new Accum(2); +warm(function() { callAddScaled(accum, 1, 3); }); +const accumBefore = accum.item; +assertEq(callAddScaled(accum, 2, 5), accumBefore + 10, "argument mapping result"); +assertEq(callAddScaled(accum, -1, 5), accumBefore + 10, "branch return result"); + +// Class method path, including class-created prototype methods. +class ClassSeq { + constructor(start) { + this.item = start; + } + + next() { + const old = this.item; + this.item = old + 4; + return old; + } +} + +function callClassSeq(obj) { + return obj.next(); +} + +const classSeq = new ClassSeq(11); +warm(function() { callClassSeq(classSeq); }); +assertEq(callClassSeq(classSeq), 1211, "class inline result"); +assertEq(classSeq.item, 1215, "class inline store"); + +console.log("jit method inlining stress: ok"); -- 2.51.2