Keep speculative copies in typed-array loops over shared storage (#2184)

This commit is contained in:
Chris Tate
2026-10-24 23:57:11 -05:00
committed by GitHub
2 changed files with 37 additions and 2 deletions
@@ -2061,7 +2061,8 @@ function emitCallbackVersionedLoop(
/** The copy of a versioned loop that runs when its guard fails. Its loops
* get no speculative or int32 kind copies, which would emit each loop again
* for a path that rarely runs. Proven integer loops, which emit one copy,
* are kept. */
* are kept. A typed-array version whose guard requires thread-owned
* storage does not use it (emitVersionedLoop). */
function emitSlowCopy(emitter: LlEmitter, s: IrStmt): void {
const copies = emitter.loopCopiesEnabled;
emitter.loopCopiesEnabled = false;
@@ -2145,7 +2146,11 @@ function emitVersionedLoop(
LOOPS.of(emitter).bytesLoopVersioning.add(s);
try {
B.startBlock(slow);
emitSlowCopy(emitter, s);
// A guard that requires thread-owned storage fails for every view of
// shared memory, which worker programs pass routinely: that checked
// copy keeps its loops' speculative and int32 kind copies.
if (version.ownedReceivers.length > 0) emitter.emitStmt(s);
else emitSlowCopy(emitter, s);
B.br(done);
if (version.owned.size > 0) emitter.bytesOwned = new Set([...originalOwned, ...version.owned]);
if (version.ranges.size > 0)
@@ -129,3 +129,33 @@ if (isMainThread) {
expect(order.slice(slow, fast).some((l) => l.startsWith("integer.leave"))).toBe(true);
expect(order.slice(fast).some((l) => l.startsWith("integer.leave"))).toBe(true);
});
test("the shared-storage copy of a typed-array version keeps its speculative copy", async () => {
// In a program that can start a worker, the guard requires thread-owned
// storage, so every view of a SharedArrayBuffer runs the checked copy.
const llvm = await emitLlvm(`
import { Worker, isMainThread } from "node:worker_threads";
function trim(value: number, bits: number): number {
return value - Math.floor(value / Math.pow(2, bits)) * Math.pow(2, bits);
}
function key(bytes: Uint8Array, bits: number): number {
let k = 0;
for (let i = 0; i < bytes.length; i++) k = trim(k, bits) * 4 + bytes[i]!;
return k;
}
if (isMainThread) {
if (process.argv[2] === "spawn") new Worker(new URL(import.meta.url));
const shared = new Uint8Array(new SharedArrayBuffer(4));
shared.set([1, 2, 3, 0]);
console.log(key(shared, 6), key(new Uint8Array([3, 2, 1, 0]), 6));
}
`);
const fn = /^define internal double @sc_bf_\w*key[\w.]*\([^]*?^}/m.exec(llvm)?.[0] ?? "";
const order = labels(fn);
const slow = order.findIndex((l) => l.startsWith("bytes.version.slow"));
const fast = order.findIndex((l) => l.startsWith("bytes.version.fast"));
expect(slow).toBeGreaterThanOrEqual(0);
expect(fast).toBeGreaterThan(slow);
expect(order.slice(slow, fast).some((l) => l.startsWith("spec.fast"))).toBe(true);
expect(order.slice(fast).some((l) => l.startsWith("spec.fast"))).toBe(true);
});