mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Keep callback loop versions from nesting
A callback loop version emits its loop twice. A loop inside either copy could be versioned again, so a nest of loops that each call a callback built just before them (`const g = adder(i)`) doubled at every level: a three-deep nest was emitted seven times per function. A loop inside a callback version, or the versioned loop itself emitted again for each copy, is no longer versioned, so a loop nest is emitted at most twice. The outer loop keeps its version; inner loops keep their calls as they are.
This commit is contained in:
@@ -1910,9 +1910,11 @@ export function emitSwitch(emitter: LlEmitter, s: IrStmt & { kind: "switch" }):
|
||||
/** The loop copies a callback loop version needs: the bindings the loop
|
||||
* calls that the constant-callback census resolves to one function, each
|
||||
* with that function's direct entry. Only small loops of optimized
|
||||
* synchronous functions, and only bindings declared before the loop that
|
||||
* an enclosing version has not already proved, qualify: the guard reads
|
||||
* each binding before the loop runs. */
|
||||
* synchronous functions, and only bindings declared before the loop,
|
||||
* qualify: the guard reads each binding before the loop runs. Versions do
|
||||
* not nest: a loop inside either copy of a version (including the
|
||||
* versioned loop itself, emitted again for each copy) keeps its calls as
|
||||
* they are, so a loop nest is emitted at most twice. */
|
||||
function callbackVersionBindings(
|
||||
emitter: LlEmitter,
|
||||
s: IrStmt & { kind: "for" | "while" | "doWhile" },
|
||||
@@ -1920,7 +1922,7 @@ function callbackVersionBindings(
|
||||
if (
|
||||
!emitter.callbackVersionsEnabled ||
|
||||
emitter.currentConstantCallbacks.size === 0 ||
|
||||
emitter.callbackVersioning.has(s)
|
||||
emitter.callbackVersioning.size > 0
|
||||
)
|
||||
return null;
|
||||
const found = new Map<string, { ref: IrExpr & { kind: "varRef" }; direct: string }>();
|
||||
@@ -1930,12 +1932,7 @@ function callbackVersionBindings(
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
if (++cost > MAX_CALLBACK_VERSION_COST) return false;
|
||||
if (e.kind === "assignExpr") bound.add(e.localId);
|
||||
if (
|
||||
e.kind === "callValue" &&
|
||||
e.callee.kind === "varRef" &&
|
||||
!emitter.knownCallbacks.has(e.callee.localId) &&
|
||||
!found.has(e.callee.localId)
|
||||
) {
|
||||
if (e.kind === "callValue" && e.callee.kind === "varRef" && !found.has(e.callee.localId)) {
|
||||
const local = emitter.currentLocals.get(e.callee.localId);
|
||||
const target =
|
||||
local && local.tdz !== true ? emitter.knownCallbackTarget(e.callee) : undefined;
|
||||
|
||||
@@ -81,3 +81,34 @@ test("a callback declared in the loop body is not read before the loop", async (
|
||||
`);
|
||||
expect(llvm).not.toContain("callback.version");
|
||||
});
|
||||
|
||||
test("loops inside a callback version are not versioned again", async () => {
|
||||
// Each inner loop calls a callback a factory builds just before it; without
|
||||
// the limit, every copy of a version would version the next loop again.
|
||||
const llvm = await emitLlvm(`
|
||||
function adder(k: number): (v: number) => number {
|
||||
return (v: number) => v + k;
|
||||
}
|
||||
function nest(n: number, f: (v: number) => number): number {
|
||||
let s = 0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
s += f(i);
|
||||
const g = adder(i);
|
||||
for (let j = 0; j < n; j++) {
|
||||
s += g(j);
|
||||
const h = adder(j);
|
||||
for (let k = 0; k < n; k++) s += h(k);
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
console.log(nest(3, (v) => v - 1), nest(2, (v) => v * 3));
|
||||
`);
|
||||
// One version for the outer loop of each compiled `nest`; its two copies
|
||||
// keep the inner loops whole.
|
||||
const versions = [...llvm.matchAll(/^define [^\n]*@sc_bf_nest[^]*?^}/gm)].map(
|
||||
([body]) => body.match(/^callback\.version\.fast\d+:$/gm)?.length ?? 0,
|
||||
);
|
||||
expect(versions.every((count) => count <= 1)).toBe(true);
|
||||
expect(versions.some((count) => count === 1)).toBe(true);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user