Keep speculative copies in typed-array loops over shared storage

In a program that can start a worker, a typed-array loop version's guard
requires thread-owned storage, so every view of a SharedArrayBuffer runs
the version's checked copy. That copy gave its loops no speculative or
int32 kind copies, on the assumption that it rarely runs, so loops over
shared views, such as k-nucleotide's frequency count in every worker, ran
only the double copy. A version whose guard requires thread-owned
storage now keeps those copies in its checked copy.
This commit is contained in:
Chris Tate
2026-10-24 23:27:15 -05:00
parent 863a71e37c
commit 20041b4262
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);
});