Inline string releases in release builds (#1887)

This commit is contained in:
Chris Tate
2026-10-22 19:03:12 -05:00
committed by GitHub
4 changed files with 75 additions and 28 deletions
@@ -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", () => {
+36 -14
View File
@@ -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) => {