Files
scriptc/packages/cli/test/cpu-dispatch.test.ts
T
Chris Tate 07c5e79ffd Add --cpu-dispatch: x86-64-v3 copies of the program's code chosen at startup
With --cpu-dispatch, an x86-64 executable carries two copies of each
program function: the baseline code and a copy compiled for x86-64-v3.
main reads CPUID and XGETBV once. Calls between the program's functions
go to the caller's copy. A function that is also named outside the
copies (a function value, a call from main, a runtime callback or a
coroutine body) keeps a dispatcher under its own name, which tests the
level and tail-calls one copy. Functions without such a use get no
dispatcher, so their copies have the callers the original had and LLVM
inlines them the same way.

The executable still runs on any x86-64 CPU. SCRIPTC_CPU_LEVEL caps the
level at run time. The option is off by default, applies to executables
without a --target-cpu level, and is part of the executable cache key.
bench:runtime takes --baseline-cpu-dispatch and --candidate-cpu-dispatch,
and the LLVM differential suite runs each program at levels 0 and 3 with
SCRIPTC_CPU_DISPATCH=1.
2026-10-25 20:00:16 -05:00

83 lines
3.3 KiB
TypeScript

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,
);