mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-02 07:43:42 +08:00
In a program with workers, every element of a view that may be backed by a SharedArrayBuffer was read and written with relaxed atomics, so that a loop which reads one element again sees another thread's write. LLVM does not vectorize loops with atomic accesses: a byte loop over a shared Uint8Array ran at 8.5 ns per element in a program with workers and 0.14 ns in one without. In a counted loop that polls for cancellation between blocks of iterations, an element read or written at the loop's counter, outside any loop nested in the body, is now a plain load or store, like a thread-owned view's. The counter steps by a nonzero constant and the body does not assign it, so every iteration reaches a different element and no read is merged across iterations or moved out of the loop. Within one iteration, a racing write from another thread can be seen or missed, the documented gap of non-atomic shared accesses. Other accesses, Float16 elements and Atomics keep their atomic paths. Corpus 8332 runs these loops over shared views of each element type on a worker and the main thread, and matches Node's output.
77 lines
2.8 KiB
TypeScript
77 lines
2.8 KiB
TypeScript
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { expect, test } from "vitest";
|
|
import { compile, deserializeModule } from "../src/index.js";
|
|
import { emitLlvmModule } from "../src/backend/llvm/emitter.js";
|
|
|
|
async function emitted(source: string): Promise<string> {
|
|
const dir = await mkdtemp(join(tmpdir(), "scriptc-worker-shared-loop-"));
|
|
try {
|
|
const entry = join(dir, "main.ts");
|
|
const outPath = join(dir, "main.ir.json");
|
|
await writeFile(entry, source);
|
|
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"));
|
|
return emitLlvmModule(deserializeModule(await readFile(outPath, "utf8")));
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
const kernel = `
|
|
function sumTo(view: Float64Array, n: number): number {
|
|
let total = 0;
|
|
for (let i = 0; i < n; i++) total += view[i]!;
|
|
return total;
|
|
}
|
|
`;
|
|
|
|
function body(ll: string, name: string): string {
|
|
return new RegExp(`^define internal [^\\n]*@sc_bf_${name}(?:\\.i32)?\\([^]*?^}`, "m").exec(
|
|
ll,
|
|
)![0];
|
|
}
|
|
|
|
/** The instructions of the blocks whose labels start with `prefix`. */
|
|
function blocks(fn: string, prefix: string): string {
|
|
return fn
|
|
.split(/\n(?=[\w.$-]+:)/)
|
|
.filter((block) => block.startsWith(prefix))
|
|
.join("\n");
|
|
}
|
|
|
|
test("a worker program's loop over views that may be shared keeps a bounds-proven copy", async () => {
|
|
const ll = await emitted(`
|
|
import { Worker, isMainThread } from "node:worker_threads";
|
|
${kernel}
|
|
if (isMainThread) {
|
|
const view = new Float64Array(new SharedArrayBuffer(64));
|
|
console.log(sumTo(view, 8));
|
|
new Worker(new URL(import.meta.url));
|
|
}
|
|
`);
|
|
const fn = body(ll, "sumTo");
|
|
// The guard tests the bounds first, then whether the view is shared.
|
|
expect(fn).toMatch(/^bytes\.version\.owned\d*:/m);
|
|
expect(fn).toMatch(/^bytes\.version\.fast\.shared\d*:/m);
|
|
// The shared copy reads with no bounds test. Each copy is a counted loop
|
|
// that polls between blocks, so its read at the counter is a plain load
|
|
// (plainSharedElement in expr-bytes.ts).
|
|
const shared = blocks(fn, "bytes.version.fast.shared");
|
|
expect(shared).not.toMatch(/icmp (ult|uge|ugt) i64 %\w+, %\w+/);
|
|
expect(fn).toMatch(/load double, ptr %\w+/);
|
|
expect(fn).not.toMatch(/load atomic double/);
|
|
});
|
|
|
|
test("a program without workers keeps two copies of the loop", async () => {
|
|
const ll = await emitted(`${kernel}
|
|
console.log(sumTo(new Float64Array(8), 8));
|
|
`);
|
|
const fn = body(ll, "sumTo");
|
|
expect(fn).toMatch(/^bytes\.version\.fast\d*:/m);
|
|
expect(fn).not.toMatch(/bytes\.version\.fast\.shared/);
|
|
expect(fn).not.toMatch(/bytes\.version\.owned/);
|
|
});
|