mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
Inline string releases in release builds (#1887)
This commit is contained in:
@@ -28,7 +28,7 @@ import {
|
||||
} from "../../ir/ir.js";
|
||||
import {
|
||||
FN_ATTRS,
|
||||
limitInlineDynReleases,
|
||||
limitInlineReleases,
|
||||
releaseSym,
|
||||
typedRefConstructor,
|
||||
vAdapters,
|
||||
@@ -2469,7 +2469,7 @@ export function emitFfiCallbackDefs(emitter: LlEmitter): { globals: string[]; de
|
||||
|
||||
export function emitParts(emitter: LlEmitter): string[] {
|
||||
const parts = [
|
||||
...limitInlineDynReleases(emitter.emitModuleParts()),
|
||||
...limitInlineReleases(emitter.emitModuleParts(), emitter.shapeHost.rcMode),
|
||||
...emitter.disposeObserverGates(),
|
||||
];
|
||||
if (!emitter.executableTls) return parts;
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
type IrType,
|
||||
} from "../../ir/ir.js";
|
||||
import { emitLlvmModule, emitLlvmModuleSource, LlEmitter } from "./emitter.js";
|
||||
import { INLINE_STR_RELEASES_PER_FUNCTION } from "./shapes.js";
|
||||
|
||||
/** A pending-exception check: the inline active-cell test or a runtime call. */
|
||||
const PENDING_CHECK = /@scr_exc_(?:active|pending)\b/;
|
||||
@@ -201,17 +202,24 @@ test("detached exit defines the runtime opt-in symbol only when requested", () =
|
||||
expect(detached.replace("@scr_detached_exit = constant i8 1\n", "")).toBe(plain);
|
||||
});
|
||||
|
||||
test("inline RC inlines every fast path at speed and keeps string and box releases as calls in release", () => {
|
||||
test("inline RC inlines every fast path at speed and keeps box releases as calls in release", () => {
|
||||
for (const boxed of [false, true]) {
|
||||
const module = moduleFor(Array.from({ length: 4 }, call), { boxed });
|
||||
const release = emitLlvmModule(module);
|
||||
expect(release).not.toContain("@sc_rc_");
|
||||
expect(release).not.toContain("%ScrMapRc");
|
||||
// The release subset inlines string retains and dyn retains and releases
|
||||
// only; these string and box releases stay runtime calls.
|
||||
// The release subset inlines string and dyn retains and releases and Map
|
||||
// retains only: box releases stay runtime calls, and so do the string
|
||||
// releases of a function with more than INLINE_STR_RELEASES_PER_FUNCTION.
|
||||
const subset = emitLlvmModule(module, { inlineRc: "release" });
|
||||
expect(subset).not.toContain("@sc_rc_release_str");
|
||||
expect(subset).not.toContain("@sc_rc_release_box");
|
||||
expect(subset).not.toContain("call void @sc_rc_release_box(");
|
||||
const sites = (ir: string, fn: string): number => ir.split(`call void @${fn}(ptr`).length - 1;
|
||||
if (!boxed) {
|
||||
expect(sites(subset, "sc_rc_release_str")).toBe(0);
|
||||
// Every site calls the runtime, and so does the unused helper's slow path.
|
||||
expect(sites(subset, "scr_str_release")).toBe(sites(release, "scr_str_release") + 1);
|
||||
expect(sites(release, "scr_str_release")).toBeGreaterThan(INLINE_STR_RELEASES_PER_FUNCTION);
|
||||
}
|
||||
const speed = emitLlvmModule(module, { inlineRc: "speed" });
|
||||
const helper = boxed ? "sc_rc_release_box" : "sc_rc_release_str";
|
||||
const runtime = boxed ? "scr_box_release" : "scr_str_release";
|
||||
|
||||
@@ -6,8 +6,9 @@ import {
|
||||
emitRecordShapes,
|
||||
inlineRcDecls,
|
||||
INLINE_DYN_RELEASES_PER_FUNCTION,
|
||||
INLINE_STR_RELEASES_PER_FUNCTION,
|
||||
inlineRcOption,
|
||||
limitInlineDynReleases,
|
||||
limitInlineReleases,
|
||||
releaseSym,
|
||||
retainSym,
|
||||
type ShapeHost,
|
||||
@@ -132,10 +133,10 @@ describe("LLVM runtime RC symbols", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("release builds inline only string retains and dyn retains and releases", () => {
|
||||
test("release builds inline only string and dyn retains and releases and Map retains", () => {
|
||||
const { host } = declarationHost(64, "release");
|
||||
expect(retainSym(host, { kind: "string" })).toBe("@sc_rc_retain_str");
|
||||
expect(releaseSym(host, { kind: "string" })).toBe("@scr_str_release");
|
||||
expect(releaseSym(host, { kind: "string" })).toBe("@sc_rc_release_str");
|
||||
expect(retainSym(host, { kind: "dyn" })).toBe("@sc_rc_retain_dyn");
|
||||
expect(releaseSym(host, { kind: "dyn" })).toBe("@sc_rc_release_dyn");
|
||||
expect(releaseSym(host, { kind: "union", unionId: "u0" } as never)).toBe("@scr_union_release");
|
||||
@@ -251,12 +252,12 @@ describe("LLVM runtime RC symbols", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("inline dyn release budget", () => {
|
||||
const body = (name: string, sites: number): string =>
|
||||
describe("inline release budgets", () => {
|
||||
const body = (name: string, sites: number, family = "dyn"): string =>
|
||||
[
|
||||
`define internal void @${name}(ptr %v) #0 {`,
|
||||
"entry:",
|
||||
...Array.from({ length: sites }, () => " call void @sc_rc_release_dyn(ptr %v)"),
|
||||
...Array.from({ length: sites }, () => ` call void @sc_rc_release_${family}(ptr %v)`),
|
||||
" ret void",
|
||||
"}",
|
||||
"",
|
||||
@@ -266,7 +267,9 @@ describe("inline dyn release budget", () => {
|
||||
const small = body("small", INLINE_DYN_RELEASES_PER_FUNCTION);
|
||||
const large = body("large", INLINE_DYN_RELEASES_PER_FUNCTION + 1);
|
||||
// main's lines are separate parts.
|
||||
const limited = limitInlineDynReleases([`; header\n${small}`, ...large.split("\n")]).join("\n");
|
||||
const limited = limitInlineReleases([`; header\n${small}`, ...large.split("\n")], "speed").join(
|
||||
"\n",
|
||||
);
|
||||
expect(limited.startsWith("; header\n")).toBe(true);
|
||||
expect(limited).toContain(small);
|
||||
const out = limited.slice(limited.indexOf("@large("));
|
||||
@@ -275,6 +278,20 @@ describe("inline dyn release budget", () => {
|
||||
INLINE_DYN_RELEASES_PER_FUNCTION + 1,
|
||||
);
|
||||
});
|
||||
|
||||
test("a function over the string budget calls the runtime string release at every site", () => {
|
||||
const small = body("small", INLINE_STR_RELEASES_PER_FUNCTION, "str");
|
||||
const large = body("large", INLINE_STR_RELEASES_PER_FUNCTION + 1, "str");
|
||||
const limited = limitInlineReleases([small, large], "release").join("\n");
|
||||
expect(limited).toContain(small);
|
||||
// Speed emission keeps every inline string release.
|
||||
expect(limitInlineReleases([small, large], "speed").join("\n")).toBe([small, large].join("\n"));
|
||||
const out = limited.slice(limited.indexOf("@large("));
|
||||
expect(out).not.toContain("@sc_rc_release_str(");
|
||||
expect(out.split("call void @scr_str_release(ptr %v)").length - 1).toBe(
|
||||
INLINE_STR_RELEASES_PER_FUNCTION + 1,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("emitted object releases", () => {
|
||||
|
||||
@@ -578,15 +578,17 @@ export function inlineRcFamily(host: ShapeHost, stem: string, op: RcOp): InlineR
|
||||
}
|
||||
|
||||
/** Which RC fast paths an emission inlines. `speed` mirrors every family
|
||||
* with a fast path. `release` mirrors only the string and Map retains and
|
||||
* the dyn retain and release: the operations that dominate dynamic-value,
|
||||
* string and Map code (a Map read from an array or another Map), at almost
|
||||
* no size cost. String releases stay calls in `release` because
|
||||
* string-heavy programs have many release sites. */
|
||||
* with a fast path. `release` mirrors only the string retain and release,
|
||||
* the Map retain and the dyn retain and release: the operations that
|
||||
* dominate dynamic-value, string and Map code (a Map read from an array or
|
||||
* another Map). String-heavy programs have many string release sites, so a
|
||||
* function with more than INLINE_STR_RELEASES_PER_FUNCTION of them calls the
|
||||
* runtime release at each (limitInlineReleases). */
|
||||
export type InlineRcMode = "release" | "speed";
|
||||
|
||||
const RELEASE_RC_OPS = new Set([
|
||||
"str:retain",
|
||||
"str:release",
|
||||
"map:retain",
|
||||
"dyn:retain",
|
||||
"dyn:release",
|
||||
@@ -670,12 +672,34 @@ function inlineRcName(family: string, op: RcOp): string {
|
||||
* the inline helper mirrors, at every site. */
|
||||
export const INLINE_DYN_RELEASES_PER_FUNCTION = 256;
|
||||
|
||||
/** Apply {@link INLINE_DYN_RELEASES_PER_FUNCTION} to each function of an
|
||||
* emitted module, given as parts joined by newlines. A function can span
|
||||
* parts (main's lines) and a part can hold several functions. */
|
||||
export function limitInlineDynReleases(parts: readonly string[]): string[] {
|
||||
const call = `call void @${inlineRcName("dyn", "release")}(`;
|
||||
const runtime = `call void @${INLINE_RC_FAMILIES["dyn"]!.release}(`;
|
||||
/** Inlined string releases one function body keeps in `release` emission,
|
||||
* which inlines string releases (RELEASE_RC_OPS); the bodies with the most of them, such as
|
||||
* builders of string-heavy records and module initializers, would grow the
|
||||
* most for the least gain, so a body with more calls scr_str_release at
|
||||
* every site. */
|
||||
export const INLINE_STR_RELEASES_PER_FUNCTION = 8;
|
||||
|
||||
/** Apply {@link INLINE_DYN_RELEASES_PER_FUNCTION}, and in `release` emission
|
||||
* {@link INLINE_STR_RELEASES_PER_FUNCTION}, to each function of an emitted
|
||||
* module, given as parts joined by newlines. A function can span parts
|
||||
* (main's lines) and a part can hold several functions. */
|
||||
export function limitInlineReleases(
|
||||
parts: readonly string[],
|
||||
mode: InlineRcMode | undefined,
|
||||
): string[] {
|
||||
const dyn = limitInlineReleaseFamily(parts, "dyn", INLINE_DYN_RELEASES_PER_FUNCTION);
|
||||
return mode === "release"
|
||||
? limitInlineReleaseFamily(dyn, "str", INLINE_STR_RELEASES_PER_FUNCTION)
|
||||
: dyn;
|
||||
}
|
||||
|
||||
function limitInlineReleaseFamily(
|
||||
parts: readonly string[],
|
||||
family: "dyn" | "str",
|
||||
max: number,
|
||||
): string[] {
|
||||
const call = `call void @${inlineRcName(family, "release")}(`;
|
||||
const runtime = `call void @${INLINE_RC_FAMILIES[family]!.release}(`;
|
||||
// The function each release site belongs to: the last `define` line
|
||||
// before it, counted over the joined text.
|
||||
const sites = (
|
||||
@@ -701,9 +725,7 @@ export function limitInlineDynReleases(parts: readonly string[]): string[] {
|
||||
const counts = new Map<number, number>();
|
||||
let fn = 0;
|
||||
for (const part of parts) fn = sites(part, (_, f) => counts.set(f, (counts.get(f) ?? 0) + 1), fn);
|
||||
const over = new Set(
|
||||
[...counts].filter(([, n]) => n > INLINE_DYN_RELEASES_PER_FUNCTION).map(([f]) => f),
|
||||
);
|
||||
const over = new Set([...counts].filter(([, n]) => n > max).map(([f]) => f));
|
||||
if (over.size === 0) return [...parts];
|
||||
fn = 0;
|
||||
return parts.map((part) => {
|
||||
|
||||
Reference in New Issue
Block a user