Add --cpu-dispatch for x86-64 executables (#2288)

This commit is contained in:
Chris Tate
2026-10-25 21:36:00 -05:00
committed by GitHub
19 changed files with 692 additions and 17 deletions
+19
View File
@@ -163,6 +163,9 @@ With `--sparse`, the profile leaves out the functions the runs never reached. Th
<dt><code>--target-cpu &lt;baseline|x86-64-v2|x86-64-v3|x86-64-v4|native&gt;</code></dt>
<dd>On x86-64 targets, compile the program's code for an x86-64 microarchitecture level. The default, <code>baseline</code>, runs on any x86-64 CPU. A higher level lets LLVM use more instructions, and the executable then runs only on CPUs that support that level. <code>native</code> uses the highest level the build machine supports. Valid for executable, assembly, and object output on x86-64 targets. See <a href="#target-cpu">Target CPU</a>.</dd>
<dt><code>--cpu-dispatch</code></dt>
<dd>On x86-64 targets, compile the program's code twice, for the baseline and for <code>x86-64-v3</code>, and choose the copy when the program starts. The executable runs on any x86-64 CPU and uses the <code>x86-64-v3</code> code on CPUs that support it. Not used with a <code>--target-cpu</code> level above <code>baseline</code>. Valid only for executable builds. See <a href="#runtime-cpu-dispatch">Runtime CPU dispatch</a>.</dd>
<dt><code>--profile-generate &lt;pattern&gt;</code></dt>
<dd>On Linux, build an instrumented executable for profile-guided optimization. Each run writes a raw profile to the path pattern, where <code>%p</code> expands to the process ID; the <code>LLVM_PROFILE_FILE</code> environment variable overrides the pattern at run time. A relative pattern resolves from the current working directory. Instrumented programs run slower. Valid only for executable builds with <code>--optimization=release</code> or <code>speed</code>.</dd>
@@ -296,6 +299,22 @@ An executable built for a level runs only on CPUs that support that level. On an
The level applies to the program's own code. The runtime pack keeps its baseline code. Builds for different levels never share cached artifacts, and `baseline` builds are the same as builds without the option.
### Runtime CPU dispatch
`--cpu-dispatch` keeps the executable portable and still uses the higher level where the CPU has it. The program's functions are compiled for the baseline and for `x86-64-v3`. At startup, the executable checks the CPU and the operating system's register support, and then runs the `x86-64-v3` code on CPUs that support every feature of that level. Calls between the program's functions stay within the chosen copy.
```console
$ scriptc build app.ts --optimization=speed --cpu-dispatch -o app
```
The second copy makes the executable larger and the first build of the program slower, since LLVM compiles the program's code twice. Unchanged rebuilds are not affected. Both copies compute the same results: neither contracts floating-point operations into fused multiply-adds.
The `SCRIPTC_CPU_LEVEL` environment variable caps the level the executable uses, for example to test the baseline code on a newer machine. It takes a level from `0` to `4`, `baseline`, or a level name such as `x86-64-v3`; any other value selects the baseline code.
```console
$ SCRIPTC_CPU_LEVEL=baseline ./app
```
## LLVM backend
scriptc generates LLVM IR and compiles it to native code. Executable builds link the result with the matching precompiled runtime pack. Use <code>--emit=llvm</code> to inspect the generated code.
+2
View File
@@ -141,6 +141,7 @@ async function tryFastPath(): Promise<number | null> {
...(optimization === "dev" || optimization === "speed" ? { optimization } : {}),
...(values.strip ? { strip: true as const } : {}),
...(values["detached-exit"] ? { detachedExit: true as const } : {}),
...(values["cpu-dispatch"] === true ? { cpuDispatch: true as const } : {}),
...(checker === undefined ? {} : { checker }),
npmStatic,
ffiProfile: ffiPath === null ? null : { path: ffiPath, bytes: ffiBytes! },
@@ -220,6 +221,7 @@ async function coverageFastPath(
values.strip ||
values["detached-exit"] ||
values["target-cpu"] !== undefined ||
values["cpu-dispatch"] !== undefined ||
values.lib ||
values.profile !== undefined ||
values.ffi !== undefined ||
+82
View File
@@ -0,0 +1,82 @@
import { execFile } from "node:child_process";
import { createRequire } from "node:module";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { promisify } from "node:util";
import { expect, test } from "vitest";
const execFileAsync = promisify(execFile);
const require = createRequire(import.meta.url);
const repoRoot = join(import.meta.dirname, "../../..");
const cliEntry = join(repoRoot, "packages/cli/src/main.ts");
const bootstrapEntry = join(repoRoot, "packages/cli/dist/bootstrap.js");
const tsxLoader = join(dirname(require.resolve("tsx/package.json")), "dist/loader.mjs");
const program = `function mix(a: Float64Array, b: Float64Array, k: number): number {
let s = 0;
for (let i = 0; i < a.length; i++) s += a[i]! * k + b[i]! / (i + 1);
return s;
}
const a = new Float64Array(4096);
const b = new Float64Array(4096);
for (let i = 0; i < a.length; i++) {
a[i] = ((i * 7919) % 1000) / 7 - 50;
b[i] = 1 / (i + 3);
}
let total = 0;
for (let r = 0; r < 32; r++) total += mix(a, b, r * 0.1);
console.log(total, [3, 1, 2].map((x) => mix(a.subarray(x), b, x)).join(" "));
`;
test.runIf(process.platform === "linux" && process.arch === "x64")(
"--cpu-dispatch builds one executable that matches Node at every level",
async () => {
const dir = await mkdtemp(join("/tmp", "scriptc-cpu-dispatch-"));
const entry = join(dir, "main.ts");
const outPath = join(dir, "program");
const llvmPath = join(dir, "main.ll");
const env = { ...process.env, SCRIPTC_CACHE_DIR: join(dir, "cache") };
const build = async (args: string[], bootstrap = false): Promise<string> => {
await execFileAsync(
process.execPath,
[
...(bootstrap ? [bootstrapEntry] : ["--import", tsxLoader, cliEntry]),
"build",
entry,
"-o",
outPath,
...args,
],
{ env, maxBuffer: 4 * 1024 * 1024 },
);
return readFile(llvmPath, "utf8");
};
const run = async (level?: string): Promise<string> =>
(
await execFileAsync(outPath, [], {
env: { ...process.env, ...(level === undefined ? {} : { SCRIPTC_CPU_LEVEL: level }) },
})
).stdout;
try {
await writeFile(entry, program);
const expected = `${(await execFileAsync(process.execPath, [entry])).stdout}`;
const plain = await build(["--optimization=speed"]);
expect(plain).not.toContain("@sc_cpu_level");
const dispatching = await build(["--optimization=speed", "--cpu-dispatch"]);
expect(dispatching).toMatch(/^define internal double @sc_bf_mix\.v3\(/m);
for (const level of [undefined, "0", "baseline", "3", "x86-64-v3"])
expect(await run(level), `SCRIPTC_CPU_LEVEL=${level}`).toBe(expected);
// The bootstrap's cached route keeps the two builds apart.
expect(await build(["--optimization=speed"], true)).toBe(plain);
expect(await build(["--optimization=speed", "--cpu-dispatch"], true)).toBe(dispatching);
expect(await build(["--cpu-dispatch"])).toContain("@sc_cpu_level_init()");
expect(await run("0")).toBe(expected);
expect(await build(["--target-cpu=x86-64-v3", "--cpu-dispatch"])).not.toContain(
"@sc_cpu_level",
);
} finally {
await rm(dir, { recursive: true, force: true });
}
},
240_000,
);
+1
View File
@@ -48,6 +48,7 @@ describe("output option compatibility", () => {
[{ emit: "llvm", strip: true }, /strip/],
[{ emit: "obj", detachedExit: true }, /detached-exit/],
[{ emit: "llvm", detachedExit: true }, /detached-exit/],
[{ emit: "asm", cpuDispatch: true }, /cpu-dispatch/],
[{ emit: "obj", profileGenerate: "/tmp/%p.profraw" }, /profile-generate/],
[{ emit: "llvm", profileUse: "/tmp/a.profdata" }, /profile-use/],
[{ profileGenerate: "/tmp/%p.profraw", profileUse: "/tmp/a.profdata" }, /cannot be combined/],
@@ -0,0 +1,330 @@
/* Runtime CPU dispatch for x86-64 executables (cloneForCpuDispatch): the
* program's functions get a copy compiled for x86-64-v3, chosen once per
* call by the CPU level the executable detects at startup. */
import type { TargetCpu } from "../target-cpu.js";
/** The levels a dispatching executable carries a copy of its program's
* functions for, above the baseline. */
export const CPU_DISPATCH_TIERS: readonly TargetCpu[] = ["x86-64-v3"];
/** The CPU level the executable runs at: 1 (the x86-64 baseline) or 3
* (x86-64-v3), set by @sc_cpu_level_init before the program runs. */
const LEVEL = "@sc_cpu_level";
/** The attribute groups of the x86-64-v3 copies: #0's and #2's (see
* emit-module.ts functionAttributes) with the level's `target-cpu`. */
const COPY_GROUPS: ReadonlyMap<string, string> = new Map([
["0", "3"],
["2", "4"],
]);
export function cpuDispatchAttributes(asan: string): string[] {
return [
`attributes #3 = {${asan} "target-cpu"="x86-64-v3" }`,
`attributes #4 = { noinline${asan} "target-cpu"="x86-64-v3" }`,
];
}
/** Bits of CPUID leaf 1's ECX that x86-64-v3 needs: SSE3 (0), SSSE3 (9),
* FMA (12), CMPXCHG16B (13), SSE4.1 (19), SSE4.2 (20), MOVBE (22), POPCNT
* (23), XSAVE (26), OSXSAVE (27), AVX (28) and F16C (29). */
const LEAF1_ECX = 0x3cd83201;
/** Leaf 7's EBX: BMI1 (3), AVX2 (5) and BMI2 (8). */
const LEAF7_EBX = 0x128;
/** Leaf 0x80000001's ECX: LAHF/SAHF (0) and LZCNT (5). */
const EXT1_ECX = 0x21;
/** `SCRIPTC_CPU_LEVEL` values and the level each caps the executable at. */
const LEVEL_NAMES: readonly [string, number][] = [
["0", 0],
["1", 1],
["2", 2],
["3", 3],
["4", 4],
["baseline", 1],
["v1", 1],
["v2", 2],
["v3", 3],
["v4", 4],
["x86-64", 1],
["x86-64-v2", 2],
["x86-64-v3", 3],
["x86-64-v4", 4],
];
/** @sc_cpu_level and @sc_cpu_level_init, which main calls first:
* - CPUID reports every x86-64-v3 feature, and XGETBV that the operating
* system saves the SSE and AVX registers, for level 3; otherwise 1;
* - `SCRIPTC_CPU_LEVEL` (a level from 0 to 4, `baseline` or an `x86-64-vN`
* name) caps the level; another value caps it at 0. */
export function cpuLevelDefs(declare: (line: string) => void): string[] {
declare(`declare ptr @getenv(ptr)`);
declare(`declare i32 @strcmp(ptr, ptr)`);
const bytes = (text: string): string => `[${text.length + 1} x i8] c"${text}\\00"`;
const lines = [
`${LEVEL} = internal global i32 1`,
`@sc_cpu_level_env = private unnamed_addr constant ${bytes("SCRIPTC_CPU_LEVEL")}`,
...LEVEL_NAMES.map(
([name], i) => `@sc_cpu_level_name_${i} = private unnamed_addr constant ${bytes(name)}`,
),
`define internal void @sc_cpu_level_init() #0 {`,
`entry:`,
` %max = call { i32, i32, i32, i32 } asm "cpuid", "={ax},={bx},={cx},={dx},{ax},{cx}"(i32 0, i32 0)`,
` %max.leaf = extractvalue { i32, i32, i32, i32 } %max, 0`,
` %has.leaf7 = icmp uge i32 %max.leaf, 7`,
` br i1 %has.leaf7, label %leaf1, label %detected`,
`leaf1:`,
` %l1 = call { i32, i32, i32, i32 } asm "cpuid", "={ax},={bx},={cx},={dx},{ax},{cx}"(i32 1, i32 0)`,
` %l1.ecx = extractvalue { i32, i32, i32, i32 } %l1, 2`,
` %l1.bits = and i32 %l1.ecx, ${LEAF1_ECX}`,
` %l1.ok = icmp eq i32 %l1.bits, ${LEAF1_ECX}`,
` br i1 %l1.ok, label %leaf7, label %detected`,
`leaf7:`,
` %l7 = call { i32, i32, i32, i32 } asm "cpuid", "={ax},={bx},={cx},={dx},{ax},{cx}"(i32 7, i32 0)`,
` %l7.ebx = extractvalue { i32, i32, i32, i32 } %l7, 1`,
` %l7.bits = and i32 %l7.ebx, ${LEAF7_EBX}`,
` %l7.ok = icmp eq i32 %l7.bits, ${LEAF7_EBX}`,
` %ext = call { i32, i32, i32, i32 } asm "cpuid", "={ax},={bx},={cx},={dx},{ax},{cx}"(i32 -2147483648, i32 0)`,
` %ext.max = extractvalue { i32, i32, i32, i32 } %ext, 0`,
` %ext.has = icmp uge i32 %ext.max, -2147483647`,
` %l7.ext = and i1 %l7.ok, %ext.has`,
` br i1 %l7.ext, label %ext1, label %detected`,
`ext1:`,
` %e1 = call { i32, i32, i32, i32 } asm "cpuid", "={ax},={bx},={cx},={dx},{ax},{cx}"(i32 -2147483647, i32 0)`,
` %e1.ecx = extractvalue { i32, i32, i32, i32 } %e1, 2`,
` %e1.bits = and i32 %e1.ecx, ${EXT1_ECX}`,
` %e1.ok = icmp eq i32 %e1.bits, ${EXT1_ECX}`,
` br i1 %e1.ok, label %xcr0, label %detected`,
// Leaf 1 reported OSXSAVE, so XGETBV is available.
`xcr0:`,
` %xcr = call { i32, i32 } asm "xgetbv", "={ax},={dx},{cx}"(i32 0)`,
` %xcr.lo = extractvalue { i32, i32 } %xcr, 0`,
` %xcr.bits = and i32 %xcr.lo, 6`,
` %xcr.ok = icmp eq i32 %xcr.bits, 6`,
` %v3 = select i1 %xcr.ok, i32 3, i32 1`,
` br label %detected`,
`detected:`,
` %level = phi i32 [ 1, %entry ], [ 1, %leaf1 ], [ 1, %leaf7 ], [ 1, %ext1 ], [ %v3, %xcr0 ]`,
` %env = call ptr @getenv(ptr @sc_cpu_level_env)`,
` %has.env = icmp ne ptr %env, null`,
` br i1 %has.env, label %cap, label %store`,
`cap:`,
];
// The matching name's level, or 0 when none matches.
let cap = "0";
LEVEL_NAMES.forEach(([, level], i) => {
lines.push(
` %name.${i} = call i32 @strcmp(ptr %env, ptr @sc_cpu_level_name_${i})`,
` %is.${i} = icmp eq i32 %name.${i}, 0`,
` %cap.${i} = select i1 %is.${i}, i32 ${level}, i32 ${cap}`,
);
cap = `%cap.${i}`;
});
lines.push(
` %capped.below = icmp ult i32 ${cap}, %level`,
` %capped = select i1 %capped.below, i32 ${cap}, i32 %level`,
` br label %store`,
`store:`,
` %final = phi i32 [ %level, %detected ], [ %capped, %cap ]`,
` store i32 %final, ptr ${LEVEL}`,
` ret void`,
`}`,
``,
);
return lines;
}
/** The call main makes before anything else. */
export const CPU_LEVEL_INIT_CALL = ` call void @sc_cpu_level_init()`;
/** A function definition's header, split around its name and parameters. */
interface Header {
/** `define` and the linkage, up to the return type. */
linkage: string;
/** The return type with its attributes. */
ret: string;
name: string;
/** The parameter list between the parentheses. */
params: string;
/** Function attributes before the group (`noinline`, `inlinehint`). */
attrs: string;
group: string;
/** What follows the group: a personality, then `{` and a comment. */
rest: string;
}
const HEADER_START = /^define ((?:internal |private )?)(.*?)@([\w.$-]+)\(/;
const HEADER_END = /^((?: [a-z_]+)*) #(\d+)( .*)$/;
const RETURN_ATTRIBUTES =
/\b(?:zeroext|signext|noalias|nonnull|noundef|inreg|dereferenceable(?:_or_null)?\(\d+\)|align \d+)\s+/g;
function parseHeader(line: string): Header | null {
const start = HEADER_START.exec(line);
if (start === null) return null;
const open = start[0].length - 1;
let depth = 0;
let close = open;
for (; close < line.length; close++) {
const c = line.charAt(close);
if (c === "(") depth++;
else if (c === ")" && --depth === 0) break;
}
const end = HEADER_END.exec(line.slice(close + 1));
if (end === null) return null;
return {
linkage: `define ${start[1]!}`,
ret: start[2]!.trim(),
name: start[3]!,
params: line.slice(open + 1, close),
attrs: end[1]!,
group: end[2]!,
rest: end[3]!,
};
}
/** The parameters split at their top-level commas. */
function splitParams(params: string): string[] {
const out: string[] = [];
let depth = 0;
let from = 0;
for (let i = 0; i < params.length; i++) {
const c = params.charAt(i);
if (c === "(" || c === "{" || c === "[" || c === "<") depth++;
else if (c === ")" || c === "}" || c === "]" || c === ">") depth--;
else if (c === "," && depth === 0) {
out.push(params.slice(from, i).trim());
from = i + 1;
}
}
if (params.trim() !== "") out.push(params.slice(from).trim());
return out;
}
/** A definition the pass copies: one in an attribute group it has a copy
* group for (coroutine bodies, #1, keep their one body), with named
* parameters, no variable arguments and no debug locations. */
function cloneable(header: Header, text: string): boolean {
if (!COPY_GROUPS.has(header.group)) return false;
if (text.includes("!dbg")) return false;
return splitParams(header.params).every((param) => /%[\w.$-]+$/.test(param));
}
/** The dispatching definition under the function's own name: it tail-calls
* the x86-64-v3 copy at level 3 or higher and the baseline copy otherwise. */
function dispatcher(header: Header): string {
const args = splitParams(header.params).join(", ");
const type = header.ret.replace(RETURN_ATTRIBUTES, "").trim();
const call = (tier: string, result: string): string[] =>
type === "void"
? [` musttail call void @${header.name}.${tier}(${args})`, ` ret void`]
: [
` ${result} = musttail call ${header.ret} @${header.name}.${tier}(${args})`,
` ret ${type} ${result}`,
];
return [
`${header.linkage}${header.ret} @${header.name}(${header.params}) #0 {`,
`entry:`,
` %sc.cpu.level = load i32, ptr ${LEVEL}`,
` %sc.cpu.v3 = icmp uge i32 %sc.cpu.level, 3`,
` br i1 %sc.cpu.v3, label %sc.cpu.v3.call, label %sc.cpu.base.call`,
`sc.cpu.v3.call:`,
...call("v3", "%sc.cpu.v3.result"),
`sc.cpu.base.call:`,
...call("base", "%sc.cpu.base.result"),
`}`,
].join("\n");
}
/** A definition split at its header line and its closing brace. */
interface Definition {
header: Header;
/** The lines after the header, through the closing `}`. */
body: string;
/** Text after the closing brace, kept as it is. */
after: string;
}
/** Rewrites the program's function definitions in `parts` for runtime
* dispatch. A definition is a part, or a `define` chunk of a part, that
* follows a partition group line (emit-module.ts partitionGroupLines);
* runtime helpers and the module's entry points stay as they are. Each
* cloneable function `@f` becomes:
* - `@f.base`, its body as before;
* - `@f.v3`, the same body in an x86-64-v3 attribute group;
* - `@f`, a dispatcher with the same signature, which every reference to
* the function as a value and every call from outside the program's
* functions still names; a function with no such reference gets none.
* Direct calls between cloned functions go to the caller's own tier, so the
* level is tested once on entry into the program's code. */
export function cloneForCpuDispatch(parts: readonly string[]): string[] {
const program: { index: number; chunks: string[] }[] = [];
for (let i = 1; i < parts.length; i++)
if (parts[i - 1]!.startsWith("; scriptc-group") && parts[i]!.startsWith("define "))
program.push({ index: i, chunks: parts[i]!.split(/\n(?=define )/) });
const defs = new Map<string, Definition>();
for (const { chunks } of program)
for (const chunk of chunks) {
const newline = chunk.indexOf("\n");
const header = newline < 0 ? null : parseHeader(chunk.slice(0, newline));
if (header === null) continue;
const close = chunk.indexOf("\n}", newline);
if (close < 0) continue;
const body = chunk.slice(newline + 1, close + 2);
if (!cloneable(header, body)) continue;
defs.set(header.name, { header, body, after: chunk.slice(close + 2) });
}
if (defs.size === 0) return [...parts];
const callee = /@([\w.$-]+)\(/g;
const retarget = (body: string, tier: string): string =>
body.replace(callee, (all, name: string) => (defs.has(name) ? `@${name}.${tier}(` : all));
const named = namedFunctions(parts, program, defs, (body) => retarget(body, "base"));
const out = [...parts];
for (const { index, chunks } of program)
out[index] = chunks
.map((chunk) => {
const header = HEADER_START.exec(chunk);
const def = header === null ? undefined : defs.get(header[3]!);
if (def === undefined) return chunk;
const h = def.header;
const copy = (tier: string, group: string): string =>
`define internal ${h.ret} @${h.name}.${tier}(${h.params})${h.attrs} #${group}${h.rest}\n${retarget(def.body, tier)}`;
return (
[
copy("base", h.group),
copy("v3", COPY_GROUPS.get(h.group)!),
...(named.has(h.name) ? [dispatcher(h)] : []),
].join("\n") + def.after
);
})
.join("\n");
return out;
}
/** The cloned functions whose own name the module still uses once the
* copies call each other: as a value, or in a call from code that is not
* copied (main, runtime helpers, coroutine bodies). Only these get a
* dispatcher. A function without one keeps the callers it had, so LLVM
* inlines and removes its copies as it would the original. */
function namedFunctions(
parts: readonly string[],
program: readonly { index: number; chunks: string[] }[],
defs: ReadonlyMap<string, Definition>,
retargeted: (body: string) => string,
): Set<string> {
const named = new Set<string>();
const scan = (text: string): void => {
for (const m of text.matchAll(/@([\w.$-]+)/g)) if (defs.has(m[1]!)) named.add(m[1]!);
};
const inProgram = new Set(program.map(({ index }) => index));
parts.forEach((part, i) => {
if (!inProgram.has(i)) scan(part);
});
for (const { chunks } of program)
for (const chunk of chunks) {
const header = HEADER_START.exec(chunk);
const def = header === null ? undefined : defs.get(header[3]!);
if (def === undefined) scan(chunk);
else scan(retargeted(def.body) + def.after);
}
return named;
}
@@ -65,6 +65,7 @@ import { STRING_READ_HELPERS, stringScanDecl, stringScanInline } from "./string-
import { emitLibraryIdentityLines } from "../library-identity-markers.js";
import { InternalCompilerError } from "../../errors.js";
import { targetCpuAttribute } from "../target-cpu.js";
import { CPU_LEVEL_INIT_CALL, cpuDispatchAttributes, cpuLevelDefs } from "./cpu-dispatch.js";
import type { IrFunction } from "../../ir/ir.js";
/** The function bodies emitted in `chunks` contiguous ranges, each by its
@@ -198,7 +199,8 @@ function readRange(dir: string, range: number): { result: ChunkResult; texts: st
* #1 for coroutine bodies and #2 for out-of-line record clones. Each carries
* the build's `--target-cpu` level, and `sanitize_address` in sanitized
* builds. A group needs one attribute: without either, #0 holds a string
* attribute that LLVM ignores. */
* attribute that LLVM ignores. Dispatching builds add the x86-64-v3 copies'
* #3 and #4 (cpu-dispatch.ts). */
function functionAttributes(emitter: LlEmitter, hasNoInlineRecordClone: boolean): string[] {
const cpu = targetCpuAttribute(emitter.targetCpu);
const asan = emitter.sanitizeAttributes ? " sanitize_address" : "";
@@ -207,6 +209,7 @@ function functionAttributes(emitter: LlEmitter, hasNoInlineRecordClone: boolean)
`attributes #0 = {${base} }`,
...(emitter.usesCoroutines ? [`attributes #1 = {${asan} presplitcoroutine${cpu} }`] : []),
...(hasNoInlineRecordClone ? [`attributes #2 = { noinline${asan}${cpu} }`] : []),
...(emitter.cpuDispatch ? cpuDispatchAttributes(asan) : []),
];
}
@@ -585,6 +588,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
...(emitter.publishWalkersCache?.defs() ?? []),
...emitter.publishedClassNamesDefs(),
...(emitter.regions?.moduleDefs() ?? []),
...(emitter.cpuDispatch ? cpuLevelDefs((line) => emitter.declare(line)) : []),
];
if (emitter.constantNumericTables.size > 0)
emitter.declare(`declare double @scr_arr_get_number(ptr, double)`);
@@ -1043,6 +1047,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
`define i32 @${emitter.mod.workers ? "sc_context_entry" : emitter.wasi ? "__main_argc_argv" : "main"}(i32 %argc, ptr %argv) ${FN_ATTRS} {`,
`entry:`,
...(emitter.runtimeAbiMarker ? [` call void @${RUNTIME_ABI_MARKER}()`] : []),
...(emitter.cpuDispatch && !emitter.mod.workers ? [CPU_LEVEL_INIT_CALL] : []),
...(emitter.mod.workers ? [` call void @scr_runtime_workers_v8()`] : []),
` call void @scr_init()`,
// Compressed fields hold addresses in the program-lifetime region, which
@@ -1201,6 +1206,8 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
`}`,
`define i32 @main(i32 %argc, ptr %argv) ${FN_ATTRS} {`,
`entry:`,
// Worker threads enter at sc_worker_entry and read the level main set.
...(emitter.cpuDispatch ? [CPU_LEVEL_INIT_CALL] : []),
` %result = call i32 @sc_context_entry(i32 %argc, ptr %argv)`,
` call void @scr_context_cleanup()`,
` ret i32 %result`,
+17 -1
View File
@@ -147,6 +147,7 @@ import {
bridgePlainUnwindCalls,
unwindAbiFunctions,
} from "./unwind-abi.js";
import { cloneForCpuDispatch } from "./cpu-dispatch.js";
import {
mangleAsyncInlineAwaitCall,
mangleAsyncSpawn,
@@ -544,6 +545,11 @@ export interface LlvmTargetOptions {
* every emitted function carries it as its `target-cpu`. Unset, code
* runs on any x86-64 CPU. */
targetCpu?: TargetCpu;
/** Give an x86-64 executable without a `targetCpu` an x86-64-v3 copy of
* each program function, which it runs on CPUs at that level
* (cpu-dispatch.ts). Ignored for other targets, libraries and builds
* with debug information. */
cpuDispatch?: boolean;
}
/** LlvmTargetOptions.lowRegion for an executable: Linux targets without
@@ -1288,6 +1294,8 @@ export class LlEmitter {
readonly targetCpu: TargetCpu | undefined;
/** Emitted functions carry `sanitize_address` (LlvmTargetOptions). */
readonly sanitizeAttributes: boolean;
/** LlvmTargetOptions.cpuDispatch, where it applies. */
readonly cpuDispatch: boolean;
readonly wasmBoundsChecks: boolean;
/** Interned string literals: UTF-8 text → { symbol, byte length } —
* first-use order, the runtime ABI’s determinism discipline. */
@@ -1991,6 +1999,13 @@ export class LlEmitter {
this.detachedExit = options.detachedExit === true;
this.targetCpu = options.targetCpu;
this.sanitizeAttributes = options.sanitizeAttributes !== false;
this.cpuDispatch =
options.cpuDispatch === true &&
options.targetCpu === undefined &&
!this.wasi &&
mod.lib === undefined &&
this.debug === null &&
/^(x86_64|amd64|x64)\b/.test(options.targetTriple || process.arch);
this.wasmBoundsChecks =
options.wasmBoundsChecks === true ||
wasmBoundsChecks({
@@ -2497,7 +2512,8 @@ export class LlEmitter {
}
emitParts(): string[] {
const parts = emitParts(this);
return this.unwindAbi.size > 0 ? bridgePlainUnwindCalls(parts) : parts;
const bridged = this.unwindAbi.size > 0 ? bridgePlainUnwindCalls(parts) : parts;
return this.cpuDispatch ? cloneForCpuDispatch(bridged) : bridged;
}
/** The dispose-observer gates the emitted frees and record writers test
@@ -105,6 +105,11 @@ export function targetCpuField(cpu: TargetCpu | undefined): { targetCpu?: Target
return cpu === undefined || cpu === "baseline" ? {} : { targetCpu: cpu };
}
/** Cache-key parts for a dispatching build: the tiers it carries. */
export function cpuDispatchKeyParts(cpuDispatch: boolean | undefined): string[] {
return cpuDispatch === true ? ["cpu-dispatch-x86-64-v3"] : [];
}
/** The error for a level on a target that is not x86-64, or null. */
export function targetCpuTargetRefusal(
cpu: TargetCpu | undefined,
+9 -1
View File
@@ -117,6 +117,7 @@ async function main(args: string[], host: CliHost): Promise<number> {
values["windows-subsystem"] !== undefined ||
values["detached-exit"] ||
values["target-cpu"] !== undefined ||
values["cpu-dispatch"] !== undefined ||
values["profile-generate"] !== undefined ||
values["profile-use"] !== undefined ||
values.sparse ||
@@ -177,6 +178,7 @@ async function main(args: string[], host: CliHost): Promise<number> {
values["windows-subsystem"] !== undefined ||
values["detached-exit"] ||
values["target-cpu"] !== undefined ||
values["cpu-dispatch"] !== undefined ||
values["profile-generate"] !== undefined ||
values["profile-use"] !== undefined ||
(values["npm-static"] ?? []).length > 0 ||
@@ -268,6 +270,7 @@ async function main(args: string[], host: CliHost): Promise<number> {
values["windows-subsystem"] !== undefined ||
values["detached-exit"] ||
values["target-cpu"] !== undefined ||
values["cpu-dispatch"] !== undefined ||
values["profile-generate"] !== undefined ||
values["profile-use"] !== undefined ||
(values["npm-static"] ?? []).length > 0 ||
@@ -277,7 +280,7 @@ async function main(args: string[], host: CliHost): Promise<number> {
hasNodeIslandFlags(values)
) {
fail(
"scriptc build --lib takes no --backend/--emit/--print/--optimization/--strip/--windows-subsystem/--detached-exit/--target-cpu/--profile-generate/--profile-use/--npm-static/--ffi/--external-types/--tier/--explain-dynamic: the profile pins the emission and optimization, npm imports are judged automatically, outbound FFI belongs to executable builds, external type mappings belong to coverage, and library artifacts are static-tier only",
"scriptc build --lib takes no --backend/--emit/--print/--optimization/--strip/--windows-subsystem/--detached-exit/--target-cpu/--cpu-dispatch/--profile-generate/--profile-use/--npm-static/--ffi/--external-types/--tier/--explain-dynamic: the profile pins the emission and optimization, npm imports are judged automatically, outbound FFI belongs to executable builds, external type mappings belong to coverage, and library artifacts are static-tier only",
);
}
const profilePath = resolve(profileArg);
@@ -323,6 +326,9 @@ async function main(args: string[], host: CliHost): Promise<number> {
if (command === "coverage" && values["target-cpu"] !== undefined) {
fail(`--target-cpu is a build/run option\n\n${USAGE}`);
}
if (command === "coverage" && values["cpu-dispatch"] !== undefined) {
fail(`--cpu-dispatch is a build/run option\n\n${USAGE}`);
}
if (command === "coverage" && values["detached-exit"]) {
fail(`--detached-exit is a build/run option\n\n${USAGE}`);
}
@@ -503,6 +509,7 @@ async function main(args: string[], host: CliHost): Promise<number> {
...(values.optimization === undefined ? {} : { optimization: values.optimization }),
strip: values.strip,
detachedExit: values["detached-exit"],
...(values["cpu-dispatch"] === undefined ? {} : { cpuDispatch: values["cpu-dispatch"] }),
...(values["profile-generate"] === undefined
? {}
: { profileGenerate: values["profile-generate"] }),
@@ -623,6 +630,7 @@ async function main(args: string[], host: CliHost): Promise<number> {
...(windowsSubsystem !== undefined ? { windowsSubsystem } : {}),
...(values["detached-exit"] ? { detachedExit: true } : {}),
...targetCpuField(targetCpu),
...(values["cpu-dispatch"] === undefined ? {} : { cpuDispatch: values["cpu-dispatch"] }),
// The instrumented program resolves a relative pattern against its own
// working directory; resolve it here, where the user wrote it.
...(values["profile-generate"] === undefined
@@ -11,6 +11,7 @@ export interface OutputOptionValues {
strip?: boolean;
windowsSubsystem?: string;
detachedExit?: boolean;
cpuDispatch?: boolean;
profileGenerate?: string;
profileUse?: string;
ffi?: string;
@@ -75,6 +76,9 @@ export function resolveOutputOptions(
if (values.detachedExit && emit !== "exe") {
return { ok: false, message: `--detached-exit is only supported with --emit=exe` };
}
if (values.cpuDispatch && emit !== "exe") {
return { ok: false, message: `--cpu-dispatch is only supported with --emit=exe` };
}
if (values.profileGenerate !== undefined && values.profileUse !== undefined) {
return { ok: false, message: `--profile-generate and --profile-use cannot be combined` };
}
+7
View File
@@ -69,6 +69,12 @@ Options:
executable then requires that level: v3 adds AVX2, BMI2
and FMA, v4 adds AVX-512. native uses the build host's
level. The runtime keeps its baseline code
--cpu-dispatch x86-64: also compile the program's code for x86-64-v3
(AVX2), and run that copy on CPUs that support it. The
executable still runs on any x86-64 CPU. Not used with
--target-cpu above baseline. The SCRIPTC_CPU_LEVEL
environment variable (0 to 4, or a level name) caps the
level at run time (opt in; --emit=exe only)
--profile-generate <pattern>
Linux: instrument the executable for profile-guided
optimization. Each run writes a raw profile to the
@@ -161,6 +167,7 @@ export const CLI_OPTIONS = {
"windows-subsystem": { type: "string" },
"detached-exit": { type: "boolean", default: false },
"target-cpu": { type: "string" },
"cpu-dispatch": { type: "boolean" },
"profile-generate": { type: "string" },
"profile-use": { type: "string" },
sparse: { type: "boolean", default: false },
+5
View File
@@ -45,6 +45,11 @@ export interface CompileBaseOptions {
* Unset or `baseline`, the code runs on any x86-64 CPU. x86-64 targets
* only; the runtime pack keeps its baseline code. */
targetCpu?: TargetCpu;
/** Give the executable an x86-64-v3 copy of each program function, which
* it runs instead of the baseline code on CPUs that support that level
* (`SCRIPTC_CPU_LEVEL` caps the level at run time). Executables for x86-64
* targets without a `targetCpu` only; off by default. */
cpuDispatch?: boolean;
/** Instrument the executable for profile-guided optimization. Each run
* writes a raw profile to this path pattern (`%p` expands to the process
* id; `LLVM_PROFILE_FILE` overrides the pattern at run time). Linux
@@ -239,6 +239,7 @@ test("early executable keys isolate compile modes, paths, implementation, checke
{ ...f.options, detachedExit: true },
{ ...f.options, targetCpu: "x86-64-v3" },
{ ...f.options, targetCpu: "x86-64-v4" },
{ ...f.options, cpuDispatch: true },
{ ...f.options, profile: "profile-generate:/tmp/%p.profraw" },
{ ...f.options, profile: "profile-use:digest" },
{ ...f.options, checker: "tsc-ts-executable-identity" },
@@ -18,7 +18,7 @@ import { compilerReleaseVersion } from "../library/sidecar.js";
import { nativeArtifactDependenciesStillMatch } from "../backend/native/artifact-stamps.js";
import { type NativeArtifactDependency } from "../backend/native/contracts.js";
import { optimizationKeyParts } from "../backend/optimization.js";
import { type TargetCpu, targetCpuKeyParts } from "../backend/target-cpu.js";
import { type TargetCpu, cpuDispatchKeyParts, targetCpuKeyParts } from "../backend/target-cpu.js";
import { privateSiblingPath } from "../backend/build-cache.js";
import type { CompilerImplementationDependency } from "../library/compiler-self-identity.js";
import {
@@ -149,6 +149,8 @@ export interface EarlyExecutableCacheOptions {
detachedExit?: true;
/** Omitted for the x86-64 baseline (`--target-cpu`). */
targetCpu?: TargetCpu;
/** Omitted for builds without runtime CPU dispatch. */
cpuDispatch?: true;
/** Omitted without --profile-generate/--profile-use; otherwise the
* pattern, or the digest of the profile's bytes. */
profile?: string;
@@ -293,6 +295,7 @@ function cacheKey(options: EarlyExecutableCacheOptions): string {
...(options.windowsSubsystem === "gui" ? ["windows-subsystem-gui"] : []),
...(options.detachedExit ? ["detached-exit"] : []),
...targetCpuKeyParts(options.targetCpu),
...cpuDispatchKeyParts(options.cpuDispatch),
...(options.profile !== undefined ? [options.profile] : []),
...(options.checker !== undefined ? [`checker:${options.checker}`] : []),
...(options.youngNursery ? ["young-nursery"] : []),
@@ -336,6 +339,7 @@ function routeKey(options: Omit<EarlyExecutableRouteOptions, "nativeEnvironment"
if (options.windowsSubsystem === "gui") hash.update("windows-subsystem-gui\0");
if (options.detachedExit) hash.update("detached-exit\0");
for (const part of targetCpuKeyParts(options.targetCpu)) hash.update(`${part}\0`);
for (const part of cpuDispatchKeyParts(options.cpuDispatch)) hash.update(`${part}\0`);
if (options.profile !== undefined) hash.update(`${options.profile}\0`);
if (options.checker !== undefined) hash.update(`checker:${options.checker}\0`);
if (options.youngNursery) hash.update("young-nursery\0");
+15
View File
@@ -951,6 +951,7 @@ async function prepareExecutableInput(
...inlineRcOption(opts.optimization, opts.sanitize === true),
...(opts.detachedExit === true ? { detachedExit: true } : {}),
...targetCpuField(opts.targetCpu),
...(opts.cpuDispatch === true ? { cpuDispatch: true } : {}),
});
} catch (err) {
if (!(err instanceof LlvmUnsupportedError)) throw err;
@@ -1382,6 +1383,19 @@ async function compileTracked(
sourceTexts: new Map(),
};
}
if (opts.cpuDispatch === true && outputKind !== "exe") {
return {
ok: false,
diagnostics: [
nativeCodegenDiag(
"SC3002",
"--cpu-dispatch is only supported for executable output",
entryPath,
),
],
sourceTexts: new Map(),
};
}
if (opts.nativeLinkInfo === true && outputKind !== "obj") {
return {
ok: false,
@@ -1585,6 +1599,7 @@ async function compileTracked(
...(opts.windowsSubsystem === "gui" ? { windowsSubsystem: "gui" as const } : {}),
...(opts.detachedExit === true ? { detachedExit: true as const } : {}),
...targetCpuField(opts.targetCpu),
...(opts.cpuDispatch === true ? { cpuDispatch: true as const } : {}),
...(profile === undefined ? {} : { profile: nativeProfileKey(profile) }),
...checkerField(),
...(youngNurseryRequested() ? { youngNursery: true as const } : {}),
+1
View File
@@ -438,6 +438,7 @@ export function prepareNativeExecutable(
...inlineRcOption(options.optimization, options.sanitize === true),
...(options.detachedExit === true && outputKind === "exe" ? { detachedExit: true } : {}),
...targetCpuField(options.targetCpu),
...(options.cpuDispatch === true && outputKind === "exe" ? { cpuDispatch: true } : {}),
...(options.optimization === "dev" && !options.strip
? { debugSources: prepared.sourceTexts }
: {}),
@@ -0,0 +1,149 @@
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { beforeAll, expect, test } from "vitest";
import { compile, deserializeModule, type IrModule } from "../src/index.js";
import { emitLlvmModule, type LlvmTargetOptions } from "../src/backend/llvm/emitter.js";
const program = `
function dot(a: Float64Array, b: Float64Array): number {
let s = 0;
for (let i = 0; i < a.length; i++) s += a[i]! * b[i]!;
return s;
}
function scale(a: Float64Array, k: number): void {
for (let i = 0; i < a.length; i++) a[i] = a[i]! * k;
}
function positive(x: number): boolean {
return x > 0;
}
function check(x: number): number {
if (x < 0) throw new RangeError("negative");
return Math.sqrt(x);
}
async function later(x: number): Promise<number> {
await null;
return dot(new Float64Array([x]), new Float64Array([x]));
}
const a = new Float64Array(64).map((_, i) => i * 0.5);
scale(a, 1.25);
console.log(dot(a, a), [1, -2, 3].filter(positive).length);
try {
console.log(check(dot(a, a)), check(-1));
} catch (error) {
console.log((error as Error).message);
}
later(3).then((value) => console.log(value));
`;
let mod: IrModule;
beforeAll(async () => {
const dir = await mkdtemp(join(tmpdir(), "scriptc-cpu-dispatch-"));
try {
const entry = join(dir, "main.ts");
const outPath = join(dir, "main.ir.json");
await writeFile(entry, program);
const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" });
if (!result.ok)
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
mod = deserializeModule(await readFile(outPath, "utf8"));
} finally {
await rm(dir, { recursive: true, force: true });
}
}, 120_000);
const x86 = { targetTriple: "x86_64-unknown-linux-gnu", unwindAbi: true } as const;
function emit(options: LlvmTargetOptions): string {
return emitLlvmModule(mod, options);
}
/** Each definition's text by name. */
function definitions(ll: string): Map<string, string> {
const out = new Map<string, string>();
for (const m of ll.matchAll(/^define [^\n]*?@([\w.$-]+)\([^]*?^}/gm)) out.set(m[1]!, m[0]);
return out;
}
test("a dispatching x86-64 build gives each program function a baseline and an x86-64-v3 copy", () => {
const ll = emit({ ...x86, cpuDispatch: true });
const defs = definitions(ll);
const cloned = [...defs.keys()]
.filter((name) => name.endsWith(".v3"))
.map((name) => name.slice(0, -3));
expect(cloned.length).toBeGreaterThan(4);
for (const name of ["sc_bf_dot", "sc_bf_scale", "sc_f_positive", "sc_f_check"])
expect(
cloned.some((c) => c.startsWith(name)),
name,
).toBe(true);
const dispatched = cloned.filter((name) => defs.has(name));
for (const name of cloned) {
const v3 = defs.get(`${name}.v3`)!;
expect(defs.get(`${name}.base`), name).toBeDefined();
expect(v3.split("\n", 1)[0]).toMatch(/ #[34]( |$)/);
// The function's own name, where the module still uses it, dispatches
// on the level.
const dispatcher = defs.get(name);
if (dispatcher === undefined) {
expect(ll, name).not.toMatch(new RegExp(`@${name.replace(/[.$]/g, "\\$&")}(?![\\w.$-])`));
continue;
}
expect(dispatcher).toContain("load i32, ptr @sc_cpu_level");
expect(dispatcher).toContain(`musttail call`);
expect(dispatcher).toContain(`@${name}.v3(`);
expect(dispatcher).toContain(`@${name}.base(`);
// A copy calls the other cloned functions' copies of its own tier.
for (const call of v3.matchAll(/@([\w.$-]+)\(/g)) {
const callee = call[1]!;
expect(cloned.includes(callee), `${name}.v3 calls ${callee}`).toBe(false);
if (callee.endsWith(".base")) throw new Error(`${name}.v3 calls ${callee}`);
}
for (const call of defs.get(`${name}.base`)!.matchAll(/@([\w.$-]+)\(/g))
expect(call[1]!.endsWith(".v3"), `${name}.base calls ${call[1]}`).toBe(false);
}
// The program's entry (called from main) and dot (called from the
// coroutine body) keep their dispatcher. Functions only the copies call
// have none, so their copies have the callers the original had.
expect(dispatched).toContain("sc_f__x25_main");
expect(dispatched).toContain("sc_bf_dot");
for (const name of ["sc_bf_scale", "sc_f_positive", "sc_f_check"])
expect(
dispatched.filter((d) => d.startsWith(name)),
name,
).toEqual([]);
// Coroutine bodies keep one body.
const coroutines = [...defs].filter(([, text]) => / #1 /.test(text.split("\n", 1)[0]!));
expect(coroutines.length).toBeGreaterThan(0);
for (const [name] of coroutines) expect(defs.has(`${name}.v3`), name).toBe(false);
// Functions used as values are referenced by their dispatching name.
expect(ll).not.toMatch(/ptr @[\w.$-]+\.(v3|base)(?!\()/);
expect(ll).toMatch(/^attributes #3 = \{ sanitize_address "target-cpu"="x86-64-v3" \}$/m);
// Unsanitized builds leave sanitize_address out of the copies' groups too.
expect(emit({ ...x86, cpuDispatch: true, sanitizeAttributes: false })).toMatch(
/^attributes #3 = \{ "target-cpu"="x86-64-v3" \}$/m,
);
// main sets the level before anything else runs.
expect(defs.get("main")).toMatch(
/^entry:\n(?: {2}call void @scr_runtime_abi\w*\(\)\n)? call void @sc_cpu_level_init\(\)$/m,
);
// No copy contracts floating-point operations.
expect(ll).not.toMatch(
/\b(?:fadd|fsub|fmul|fdiv|frem|call) (?:[a-z]+ )*(?:contract|fast|afn|reassoc)\b/,
);
});
test("builds without dispatch, for other targets or for a target level emit no copies", () => {
const plain = emit(x86);
expect(plain).not.toContain("@sc_cpu_level");
expect(plain).not.toMatch(/\.v3\(/);
expect(emit({ ...x86, cpuDispatch: false })).toBe(plain);
const v3 = emit({ ...x86, targetCpu: "x86-64-v3" });
expect(emit({ ...x86, targetCpu: "x86-64-v3", cpuDispatch: true })).toBe(v3);
for (const options of [
{ targetTriple: "aarch64-unknown-linux-gnu" },
{ targetTriple: "wasm32-wasi", wasi: true, pointerBits: 32 as const },
])
expect(emit({ ...options, cpuDispatch: true })).toBe(emit(options));
});
+11 -4
View File
@@ -42,6 +42,8 @@ const { values } = parseArgs({
"candidate-optimization": { type: "string", default: "release" },
"baseline-target-cpu": { type: "string" },
"candidate-target-cpu": { type: "string" },
"baseline-cpu-dispatch": { type: "boolean", default: false },
"candidate-cpu-dispatch": { type: "boolean", default: false },
json: { type: "string" },
"no-lock": { type: "boolean", default: false },
node: { type: "boolean", default: true },
@@ -54,6 +56,7 @@ if (values.help) {
[--workloads=a,b] [--runs=15] [--warmup=2] [--timeout=120] [--layouts=1] [--json=<file>]
[--baseline-optimization=release] [--candidate-optimization=release]
[--baseline-target-cpu=<level>] [--candidate-target-cpu=<level>]
[--baseline-cpu-dispatch] [--candidate-cpu-dispatch]
[--no-lock] [--no-node] [--keep]
Each checkout must have a built CLI (packages/cli/dist/bootstrap.js) and native
@@ -74,8 +77,9 @@ Workload names are the file names in benchmarks/runtime/workloads/*.json; for ex
contenders, e.g. --baseline=. --candidate-optimization=speed compares speed with
release. --baseline-target-cpu/--candidate-target-cpu pass --target-cpu to each
contender's builds, e.g. --baseline=. --candidate-target-cpu=x86-64-v3 compares
x86-64-v3 code with the baseline. cold_build_ms records each contender's first,
uncached build.`);
x86-64-v3 code with the baseline. --baseline-cpu-dispatch/--candidate-cpu-dispatch
pass --cpu-dispatch, and SCRIPTC_CPU_LEVEL in the environment reaches every timed
run. cold_build_ms records each contender's first, uncached build.`);
process.exit(0);
}
const runs = Number(values.runs);
@@ -444,7 +448,7 @@ function revision(root) {
});
return r.stdout.trim() + (dirty.stdout.trim() ? "+dirty" : "");
}
function build(label, root, workload, layout, optimization, targetCpu) {
function build(label, root, workload, layout, optimization, targetCpu, cpuDispatch) {
const cli = cliFor(root);
const out = join(work, label, layout === 0 ? workload.name : `${workload.name}.layout${layout}`);
mkdirSync(dirname(out), { recursive: true });
@@ -466,6 +470,7 @@ function build(label, root, workload, layout, optimization, targetCpu) {
entry,
`--optimization=${optimization}`,
...(targetCpu === undefined ? [] : [`--target-cpu=${targetCpu}`]),
...(cpuDispatch ? ["--cpu-dispatch"] : []),
"-o",
out,
],
@@ -492,6 +497,7 @@ const contenders = [
root: resolve(values.baseline),
optimization: values["baseline-optimization"],
targetCpu: values["baseline-target-cpu"],
cpuDispatch: values["baseline-cpu-dispatch"],
},
]
: []),
@@ -500,6 +506,7 @@ const contenders = [
root: resolve(values.candidate),
optimization: values["candidate-optimization"],
targetCpu: values["candidate-target-cpu"],
cpuDispatch: values["candidate-cpu-dispatch"],
},
];
const report = {
@@ -606,7 +613,7 @@ try {
entry.results[c.label] = result;
result.binaries = [];
for (let layout = 0; layout < layouts && result.status === undefined; layout++) {
const built = build(c.label, c.root, w, layout, c.optimization, c.targetCpu);
const built = build(c.label, c.root, w, layout, c.optimization, c.targetCpu, c.cpuDispatch);
result.build_ms += Math.round(built.buildMs);
if (layout === 0) result.cold_build_ms = Math.round(built.buildMs);
if (!built.ok) {
+21 -9
View File
@@ -1,6 +1,8 @@
/* Both LLVM optimization modes must compile every corpus program and match
* Node's stdout, successful stderr, and expected exit status. Any backend
* refusal is a failure. SCRIPTC_SAN=1 instruments emitted code and runtime. */
* refusal is a failure. SCRIPTC_SAN=1 instruments emitted code and runtime.
* SCRIPTC_CPU_DISPATCH=1 builds with runtime CPU dispatch and runs each
* executable at SCRIPTC_CPU_LEVEL=0 and 3, the baseline and x86-64-v3 copies. */
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { globSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
@@ -30,6 +32,8 @@ const files = corpusSelect(
process.env["SCRIPTC_SAN"] === "1",
);
const sanitize = process.env["SCRIPTC_SAN"] === "1";
const cpuDispatch = process.env["SCRIPTC_CPU_DISPATCH"] === "1";
const cpuLevels: readonly (string | undefined)[] = cpuDispatch ? ["0", "3"] : [undefined];
const requestedMode = process.env["SCRIPTC_LLVM_TEST_MODE"];
if (requestedMode !== undefined && requestedMode !== "release" && requestedMode !== "dev") {
throw new Error("SCRIPTC_LLVM_TEST_MODE must be release or dev when set");
@@ -62,14 +66,17 @@ function expectedExitCode(file: string): number {
return 0;
}
async function runBinary(cmd: string, args: string[]): Promise<RunResult> {
async function runBinary(cmd: string, args: string[], cpuLevel?: string): Promise<RunResult> {
// Linux fork/exec race (observed on the sandbox lane): a sibling
// worker's fork can inherit the freshly-linked binary's write fd across
// its own spawn window, and exec answers ETXTBSY until that fd closes.
// The condition is transient by construction — retry briefly, the
// npm/cargo stance.
for (let attempt = 0; ; attempt++) {
const pending = execFileAsync(cmd, args, { encoding: "buffer" });
const pending = execFileAsync(cmd, args, {
encoding: "buffer",
...(cpuLevel === undefined ? {} : { env: { ...process.env, SCRIPTC_CPU_LEVEL: cpuLevel } }),
});
pending.child.stdin?.end();
try {
const { stdout, stderr } = await pending;
@@ -179,6 +186,7 @@ async function build(file: string, optimization: "release" | "dev") {
const key = hash
.update(sanitize ? "san" : "plain")
.update("llvm-" + optimization)
.update(cpuDispatch ? "cpu-dispatch" : "")
.digest("hex")
.slice(0, 16);
const outDir = join(cacheDir, key);
@@ -188,10 +196,11 @@ async function build(file: string, optimization: "release" | "dev") {
outDir,
sanitize,
optimization,
...(cpuDispatch ? { cpuDispatch } : {}),
});
}
describe(`llvm differential corpus (${files.length} programs, ${optimizationModes.join("+")}${sanitize ? ", sanitized" : ""}${shardSuffix()})`, () => {
describe(`llvm differential corpus (${files.length} programs, ${optimizationModes.join("+")}${sanitize ? ", sanitized" : ""}${cpuDispatch ? ", CPU dispatch" : ""}${shardSuffix()})`, () => {
test.for(files.map((f) => [f.slice(corpusDir.length + 1), f] as const))(
"%s",
async ([rel, file]) => {
@@ -207,11 +216,14 @@ describe(`llvm differential corpus (${files.length} programs, ${optimizationMode
);
expect(result.backend).toBe("llvm");
expect(result.llvmPath.endsWith(".ll")).toBe(true);
const actual = await runBinary(result.binaryPath, []);
expect(actual.stdout, `${optimization} stdout`).toEqual(oracle.stdout);
if (expectedExit === 0)
expect(comparableStderr(actual.stderr), `${optimization} stderr`).toEqual(oracle.stderr);
expect(actual.exitCode, `${optimization} exit status`).toBe(expectedExit);
for (const level of cpuLevels) {
const actual = await runBinary(result.binaryPath, [], level);
const lane = level === undefined ? optimization : `${optimization} level ${level}`;
expect(actual.stdout, `${lane} stdout`).toEqual(oracle.stdout);
if (expectedExit === 0)
expect(comparableStderr(actual.stderr), `${lane} stderr`).toEqual(oracle.stderr);
expect(actual.exitCode, `${lane} exit status`).toBe(expectedExit);
}
}
},
);