diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts index 156ad8b8d..b6e489c19 100644 --- a/packages/compiler/src/backend/llvm/emit-module.ts +++ b/packages/compiler/src/backend/llvm/emit-module.ts @@ -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; diff --git a/packages/compiler/src/backend/llvm/emitter.test.ts b/packages/compiler/src/backend/llvm/emitter.test.ts index f413d6153..d1fd62a83 100644 --- a/packages/compiler/src/backend/llvm/emitter.test.ts +++ b/packages/compiler/src/backend/llvm/emitter.test.ts @@ -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"; diff --git a/packages/compiler/src/backend/llvm/shapes.test.ts b/packages/compiler/src/backend/llvm/shapes.test.ts index 347099c16..f7327a688 100644 --- a/packages/compiler/src/backend/llvm/shapes.test.ts +++ b/packages/compiler/src/backend/llvm/shapes.test.ts @@ -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", () => { diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index 382d2aa34..8ae24375e 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -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(); 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) => {