From 20041b4262e9be646fa30215f5efb3300925bdea Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 23:27:15 -0500 Subject: [PATCH] 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. --- .../src/backend/llvm/emit-statements.ts | 9 ++++-- .../test/speculative-loop-emission.test.ts | 30 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index 378c82bee..63bae226e 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -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) diff --git a/packages/compiler/test/speculative-loop-emission.test.ts b/packages/compiler/test/speculative-loop-emission.test.ts index 3f6aef158..748fd50c5 100644 --- a/packages/compiler/test/speculative-loop-emission.test.ts +++ b/packages/compiler/test/speculative-loop-emission.test.ts @@ -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); +});