mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Send every dyn kind that can be a cycle candidate through one release path
The inline dyn release sorted a surviving value into three groups by kind: arrays and objects tested the candidate-buffer word and then called scr_dyn_release_survivor, capsules and handles called it directly, and functions and proxies appended to the candidate buffer inline. All six kinds now take one path: a value already in a candidate buffer, or never one, returns inline, and scr_dyn_release_survivor, which now also covers functions and proxies, decides the rest. Behavior is unchanged, and each inlined release site keeps one kind test and one call. Effect's executable text is 10% smaller and it runs 5.8% faster; zod runs 3.8% faster, and bench:runtime's dynamic-properties 14% faster.
This commit is contained in:
@@ -146,11 +146,19 @@ describe("LLVM runtime RC symbols", () => {
|
||||
// The dyn fast path buffers surviving values by kind, as scr_dyn_release does.
|
||||
const ir = emitInlineRcHelpers(host).join("\n");
|
||||
expect(ir).toContain("call void @scr_dyn_release_survivor(ptr %o)");
|
||||
expect(ir).toMatch(
|
||||
/switch i32 %kind, label %done \[ i32 4, label %container i32 5, label %container i32 8, label %root/,
|
||||
// Every kind that can be a candidate takes one path; scalars stay inline.
|
||||
expect(ir).toContain(
|
||||
"switch i32 %kind, label %done [ i32 4, label %candidate i32 5, label %candidate i32 8, label %candidate i32 9, label %candidate i32 12, label %candidate i32 13, label %candidate ]",
|
||||
);
|
||||
// Arrays and objects not yet buffered ask the runtime, which skips intact documents.
|
||||
// Values not yet buffered ask the runtime, which decides per value.
|
||||
expect(ir).toMatch(/br i1 %cqueued, label %done, label %survivor/);
|
||||
// The runtime buffers dyn candidates, so the dyn helper has no inline append.
|
||||
const dynHelper = ir.slice(
|
||||
ir.indexOf("@sc_rc_release_dyn("),
|
||||
ir.indexOf("\n}", ir.indexOf("@sc_rc_release_dyn(")),
|
||||
);
|
||||
expect(dynHelper).not.toContain("sc_cyc_append");
|
||||
expect(dynHelper).not.toContain("scr_cyc_on_release");
|
||||
});
|
||||
|
||||
test("sanitized builds call the fast-path helpers out of line", () => {
|
||||
@@ -208,36 +216,42 @@ describe("LLVM runtime RC symbols", () => {
|
||||
});
|
||||
|
||||
test("optimized builds buffer candidates through the module's append helper", () => {
|
||||
const emit = (inlineRc: true | "release", extra: Partial<ShapeHost>) => {
|
||||
const emit = (inlineRc: true | "release", extra: Partial<ShapeHost>, kind: "union" | "dyn") => {
|
||||
const { host } = declarationHost(64, inlineRc);
|
||||
const configured: ShapeHost = { ...host, inlineAlloc: true, ...extra };
|
||||
releaseSym(configured, { kind: "dyn" });
|
||||
releaseSym(configured, kind === "union" ? ({ kind, unionId: "u0" } as never) : { kind });
|
||||
return {
|
||||
ir: emitInlineRcHelpers(configured).join("\n"),
|
||||
decls: inlineRcDecls(configured),
|
||||
};
|
||||
};
|
||||
const { ir, decls } = emit(true, {}, "union");
|
||||
expect(ir).toContain("call preserve_mostcc void @sc_cyc_append(ptr %o)");
|
||||
expect(ir).toContain("define internal preserve_mostcc void @sc_cyc_append(ptr %o) noinline");
|
||||
// The young test reads the generation byte at obj-5; the append fills
|
||||
// scr_cyc_buffers' nursery buffer and the header's position at obj-4.
|
||||
expect(ir).toContain("getelementptr i8, ptr %o, i64 -5");
|
||||
expect(ir).toContain("getelementptr inbounds i8, ptr @scr_cyc_buffers, i64 96");
|
||||
expect(ir).toContain("getelementptr inbounds i8, ptr @scr_cyc_buffers, i64 104");
|
||||
expect(ir).toContain("load i64, ptr @scr_cyc_live");
|
||||
expect(ir).toContain("or i32 %i, -2147483648");
|
||||
expect(ir).toContain("call void @scr_cyc_on_release(ptr %o)");
|
||||
expect(decls).toContain(
|
||||
"@scr_cyc_buffers = external global { [3 x { ptr, i64, i64, i64 }], i64, i64 }",
|
||||
);
|
||||
expect(decls).toContain("@scr_cyc_live = external global i64");
|
||||
// The dyn release buffers through scr_dyn_release_survivor, so a module
|
||||
// whose only headered release is dyn's has no append helper.
|
||||
for (const mode of [true, "release"] as const) {
|
||||
const { ir, decls } = emit(mode, {});
|
||||
expect(ir).toContain("call preserve_mostcc void @sc_cyc_append(ptr %o)");
|
||||
expect(ir).toContain("define internal preserve_mostcc void @sc_cyc_append(ptr %o) noinline");
|
||||
// The young test reads the generation byte at obj-5; the append fills
|
||||
// scr_cyc_buffers' nursery buffer and the header's position at obj-4.
|
||||
expect(ir).toContain("getelementptr i8, ptr %o, i64 -5");
|
||||
expect(ir).toContain("getelementptr inbounds i8, ptr @scr_cyc_buffers, i64 96");
|
||||
expect(ir).toContain("getelementptr inbounds i8, ptr @scr_cyc_buffers, i64 104");
|
||||
expect(ir).toContain("load i64, ptr @scr_cyc_live");
|
||||
expect(ir).toContain("or i32 %i, -2147483648");
|
||||
expect(ir).toContain("call void @scr_cyc_on_release(ptr %o)");
|
||||
expect(decls).toContain(
|
||||
"@scr_cyc_buffers = external global { [3 x { ptr, i64, i64, i64 }], i64, i64 }",
|
||||
);
|
||||
expect(decls).toContain("@scr_cyc_live = external global i64");
|
||||
const dyn = emit(mode, {}, "dyn");
|
||||
expect(dyn.ir).not.toContain("sc_cyc_append");
|
||||
expect(dyn.ir).not.toContain("scr_cyc_on_release");
|
||||
expect(dyn.decls.join("\n")).not.toContain("scr_cyc_buffers");
|
||||
}
|
||||
// Worker executables and targets without inline allocation call
|
||||
// scr_cyc_on_release, which tries the same append first.
|
||||
for (const extra of [{ threadLocalAlloc: true }, { inlineAlloc: false }]) {
|
||||
const { ir, decls } = emit(true, extra);
|
||||
const { ir, decls } = emit(true, extra, "union");
|
||||
expect(ir).not.toContain("sc_cyc_append");
|
||||
expect(ir).toContain("call void @scr_cyc_on_release(ptr %o)");
|
||||
expect(decls.join("\n")).not.toContain("scr_cyc_buffers");
|
||||
|
||||
@@ -589,9 +589,10 @@ interface InlineRcFamily {
|
||||
* repeats its own checks, so the fast path never duplicates it.
|
||||
*
|
||||
* The dyn release buffers a surviving value by kind (scr_dyn_release):
|
||||
* the fast path decides the always and never cases and calls
|
||||
* scr_dyn_release_survivor for kinds whose candidacy is per value,
|
||||
* including arrays and objects of intact documents (ScrDynRegion).
|
||||
* the fast path skips scalars and values already in a candidate buffer or
|
||||
* never candidates, and calls scr_dyn_release_survivor for the rest, which
|
||||
* decides per value (intact documents, ScrDynRegion; capsules and handles
|
||||
* without edges).
|
||||
*
|
||||
* The array release inlines the decrement of an array that stays alive
|
||||
* and is not headered (`a->cyc`): those are most array releases in
|
||||
@@ -698,9 +699,10 @@ function requestRcHelper(host: ShapeHost, family: string, op: RcOp): string {
|
||||
for (const decl of rcHelperDecls(family, op)) host.declare(decl);
|
||||
// Its body is written last (emitInlineRcHelpers), after the requests are
|
||||
// collected: a release that can buffer a candidate requests the append
|
||||
// helper now. The jsval row (JS_RC_ROW) has no collector header; its
|
||||
// cells release through dyn's helper, which makes its own request.
|
||||
if (op === "release" && row.cycle === "always") requestCycleAppend(host);
|
||||
// helper now. The dyn release buffers through scr_dyn_release_survivor
|
||||
// instead. The jsval row (JS_RC_ROW) has no collector header; its cells
|
||||
// release through dyn's helper.
|
||||
if (op === "release" && row.cycle === "always" && family !== "dyn") requestCycleAppend(host);
|
||||
return `@${inlineRcName(family, op)}`;
|
||||
}
|
||||
|
||||
@@ -820,8 +822,11 @@ function rcHelperDecls(family: string, op: RcOp): string[] {
|
||||
if (op === "retain" || row.release === null) return [];
|
||||
return [
|
||||
`declare void @${row.release}(ptr)`,
|
||||
...(row.cycle === "always" ? [`declare void @scr_cyc_on_release(ptr)`] : []),
|
||||
...(family === "dyn" ? [`declare void @scr_dyn_release_survivor(ptr)`] : []),
|
||||
...(family === "dyn"
|
||||
? [`declare void @scr_dyn_release_survivor(ptr)`]
|
||||
: row.cycle === "always"
|
||||
? [`declare void @scr_cyc_on_release(ptr)`]
|
||||
: []),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -931,21 +936,21 @@ function releaseHelper(host: ShapeHost, family: string, row: InlineRcFamily): st
|
||||
// scr_dyn_release buffers a surviving value by kind: always for
|
||||
// functions and proxies, per value for arrays and objects (not
|
||||
// while their document is intact, ScrDynRegion), capsules and
|
||||
// handles (scr_dyn_release_survivor), never for scalars. An
|
||||
// array or object already in a candidate buffer stays inline.
|
||||
// handles, never for scalars. Every kind that can be buffered
|
||||
// takes one path, which keeps each inlined site small: a value
|
||||
// already in a candidate buffer (or never one) stays inline, and
|
||||
// scr_dyn_release_survivor decides the rest.
|
||||
` %kp = getelementptr i8, ptr %o, ${S} ${S === "i64" ? 8 : 4} ; ScrDyn.kind`,
|
||||
` %kind = load i32, ptr %kp`,
|
||||
// SCR_DYN_ARR 4, OBJ 5, FUNC 8, PROXY 13; HANDLE 9, TYPED_REF 12.
|
||||
` switch i32 %kind, label %done [ i32 4, label %container i32 5, label %container i32 8, label %root i32 13, label %root i32 9, label %survivor i32 12, label %survivor ]`,
|
||||
`container:`,
|
||||
// SCR_DYN_ARR 4, OBJ 5, FUNC 8, HANDLE 9, TYPED_REF 12, PROXY 13.
|
||||
` switch i32 %kind, label %done [ i32 4, label %candidate i32 5, label %candidate i32 8, label %candidate i32 9, label %candidate i32 12, label %candidate i32 13, label %candidate ]`,
|
||||
`candidate:`,
|
||||
` %cbufp = getelementptr i8, ptr %o, ${S} -${host.cycleBufOffset}`,
|
||||
` %cbuf = load i32, ptr %cbufp`,
|
||||
` %cqueued = icmp slt i32 %cbuf, 0`,
|
||||
` br i1 %cqueued, label %done, label %survivor`,
|
||||
`survivor:`,
|
||||
` call void @scr_dyn_release_survivor(ptr %o)`,
|
||||
` br label %done`,
|
||||
`root:`,
|
||||
]
|
||||
: family === "box"
|
||||
? [
|
||||
@@ -964,7 +969,9 @@ function releaseHelper(host: ShapeHost, family: string, row: InlineRcFamily): st
|
||||
`root:`,
|
||||
]
|
||||
: []),
|
||||
...(cycle === "always" ? cycleRootLines(host, "done") : [` br label %done`]),
|
||||
...(cycle === "always" && family !== "dyn"
|
||||
? cycleRootLines(host, "done")
|
||||
: [` br label %done`]),
|
||||
`done:`,
|
||||
` ret void`,
|
||||
`}`,
|
||||
|
||||
@@ -1941,7 +1941,8 @@ extern void scr_dyn_listener_fire_data(ScrClosure *cb, ScrBytes *chunk);
|
||||
extern void scr_dyn_listener_fire_err(ScrClosure *cb, ScrStr *msg);
|
||||
|
||||
void *scr_dyn_retain_v(void *d);
|
||||
/* After a compiled fast path decremented a surviving capsule or handle. */
|
||||
/* After a compiled fast path decremented a surviving value of a kind that
|
||||
* can be a cycle candidate and is not one yet (scr_json.c). */
|
||||
void scr_dyn_release_survivor(ScrDyn *d);
|
||||
void scr_dyn_release_v(void *d);
|
||||
void scr_dyn_trace_v(void *d, ScrTraceVisit visit, void *ctx);
|
||||
|
||||
@@ -1317,9 +1317,9 @@ static void scr_dyn_gcfree(void *ptr) { scr_dyn_dispose(ptr, true); }
|
||||
static void scr_dyn_destroy(void *ptr) { scr_dyn_dispose(ptr, false); }
|
||||
|
||||
/* The cycle bookkeeping of a value that survives a release. Compiled
|
||||
* release fast paths decrement first and call this for the kinds whose
|
||||
* candidacy depends on per-value state, matching scr_dyn_release: arrays
|
||||
* and objects not yet buffered (skipped while their document is intact,
|
||||
* release fast paths decrement first and call this for every kind that can
|
||||
* be a candidate and is not one yet, matching scr_dyn_release: functions
|
||||
* and proxies, arrays and objects (skipped while their document is intact,
|
||||
* ScrDynRegion), capsules and handles. */
|
||||
void scr_dyn_release_survivor(ScrDyn *d) {
|
||||
if (d->kind == SCR_DYN_ARR || d->kind == SCR_DYN_OBJ) {
|
||||
@@ -1327,6 +1327,10 @@ void scr_dyn_release_survivor(ScrDyn *d) {
|
||||
if (!region || !region->intact) scr_cyc_released(d);
|
||||
return;
|
||||
}
|
||||
if (d->kind == SCR_DYN_FUNC || d->kind == SCR_DYN_PROXY) {
|
||||
scr_cyc_released(d);
|
||||
return;
|
||||
}
|
||||
if ((d->kind == SCR_DYN_TYPED_REF &&
|
||||
(d->v.typed_ref.materialized || d->v.typed_ref.casts || d->v.typed_ref.traced ||
|
||||
d->prototype || d->symbol_properties || d->symbol_keys)) ||
|
||||
|
||||
Reference in New Issue
Block a user