Version loops to a typed array's length on every typed array they index (#1762)

This commit is contained in:
Chris Tate
2026-10-22 01:15:26 -05:00
committed by GitHub
11 changed files with 458 additions and 4 deletions
+1
View File
@@ -26,6 +26,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
| `region-builds` | cpu | a watch-mode build loop over `LifetimeRegion`: each build binds a program in a new region and releases the previous one, which a cache keeps referenced for two builds |
| `template-render` | cpu | template literals, escaping, `map`/`join` |
| `thread-slots`, `thread-slots-cache` | cpu | worker threads reading one published type graph from an `InternTable` and keeping per-thread state for each type they visit: a `ThreadSlots` family, or a direct-mapped cache by id in front of a `Map` by id (the `-cache` entry); under Node.js both are per thread |
| `typed-array-kernels` | cpu | element-wise loops that run to one typed array's length and index others at the same position: `Float64Array` axpy, `Float32Array` blend, `Uint8Array` mask, wrapping `Int32Array` add, RGBA to gray |
| `typed-array-scan`, `typed-array-scan-workers` | cpu | element-by-element scan loops over `Uint8Array`, `Uint16Array`, `Int32Array` and `Float64Array`; the second entry is a worker-enabled executable (it can start a worker), so each access also checks for shared storage |
| `unicode-lexer` | cpu | a hand-written lexer over a 2.7M-unit bundled module whose comments and strings are in several languages: `charCodeAt` scans, line starts |
| `validate-errors` | cpu | throw/catch on a fraction of inputs, small call chains |
@@ -0,0 +1,6 @@
{
"entry": "workloads/typed-array-kernels.ts",
"args": [
"1"
]
}
@@ -0,0 +1,92 @@
// Element-wise loops over several typed arrays: each loop runs to one
// array's length and reads or writes the others at the same index.
function axpy(a: number, x: Float64Array, y: Float64Array, out: Float64Array): void {
for (let i = 0; i < out.length; i++) out[i] = a * x[i]! + y[i]!;
}
function blend(t: number, a: Float32Array, b: Float32Array, out: Float32Array): void {
for (let i = 0; i < out.length; i++) out[i] = a[i]! * (1 - t) + b[i]! * t;
}
function mask(data: Uint8Array, key: Uint8Array, out: Uint8Array): void {
for (let i = 0; i < out.length; i++) out[i] = data[i]! ^ key[i]!;
}
function addWrap(a: Int32Array, b: Int32Array, out: Int32Array): void {
for (let i = 0; i < out.length; i++) out[i] = (a[i]! + b[i]!) | 0;
}
function gray(rgba: Uint8Array, out: Uint8Array): void {
for (let i = 0; i < out.length; i++) {
const j = i * 4;
out[i] = (rgba[j]! * 77 + rgba[j + 1]! * 150 + rgba[j + 2]! * 29) >> 8;
}
}
const scale = Number(process.argv[2] ?? "1");
const n = 1 << 16;
const x = new Float64Array(n);
const y = new Float64Array(n);
const d = new Float64Array(n);
const fa = new Float32Array(n);
const fb = new Float32Array(n);
const fo = new Float32Array(n);
const data = new Uint8Array(n);
const key = new Uint8Array(n);
const masked = new Uint8Array(n);
const ia = new Int32Array(n);
const ib = new Int32Array(n);
const io = new Int32Array(n);
const rgba = new Uint8Array(n * 4);
const luma = new Uint8Array(n);
let state = 2463534242;
const next = (): number => {
state ^= state << 13;
state ^= state >>> 17;
state ^= state << 5;
return state >>> 0;
};
for (let i = 0; i < n; i++) {
x[i] = (next() % 20000) / 100 - 100;
y[i] = (next() % 20000) / 100;
fa[i] = (next() % 1000) / 7;
fb[i] = (next() % 1000) / 9;
data[i] = next() & 255;
key[i] = next() & 255;
ia[i] = next() | 0;
ib[i] = next() | 0;
}
for (let i = 0; i < rgba.length; i++) rgba[i] = next() & 255;
const rounds = Math.round(1200 * scale);
let f64Sum = 0;
let f32Sum = 0;
let byteSum = 0;
let intSum = 0;
let lumaSum = 0;
for (let r = 0; r < rounds; r++) {
// Change one input element per round so no round repeats the last.
const at = (r * 7919) & (n - 1);
x[at] = r;
fa[at] = r;
data[at] = r & 255;
ia[at] = r * 65537;
rgba[at * 4] = r & 255;
axpy(1.5, x, y, d);
blend((r & 15) / 16, fa, fb, fo);
mask(data, key, masked);
addWrap(ia, ib, io);
gray(rgba, luma);
const probe = (r * 104729) & (n - 1);
f64Sum += d[probe]!;
f32Sum += fo[probe]!;
byteSum += masked[probe]!;
intSum = (intSum + io[probe]!) | 0;
lumaSum += luma[probe]!;
}
console.log(`float64 ${f64Sum.toFixed(3)}`);
console.log(`float32 ${f32Sum.toFixed(3)}`);
console.log(`uint8 ${byteSum}`);
console.log(`int32 ${intSum}`);
console.log(`gray ${lumaSum}`);
@@ -209,6 +209,47 @@ test("emitted loops version the body and tag typed-array memory", () => {
expect(elements).not.toBe(header);
});
test("loops to a typed array's length version the other typed arrays they index", () => {
const length = (receiver: IrExpr): IrExpr => ({
kind: "bytesIntrinsic",
method: "length",
receiver,
args: [],
type: F64,
loc,
});
const emit = (body: IrStmt[]): string => {
const mod: IrModule = {
irVersion: 17,
sourceFile: loc.file,
entry: "__main",
functions: [
{ name: "__main", params: [], returnType: VOID, locals: [], body: [], loc },
fn(body),
],
};
return /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(emitLlvmModule(mod))![0];
};
// for (let i = 0; i < out.length; i++) out[i] = src[i] * 2
const scaled = set(ref("out", f64s), ref("i"), bin("*", get(ref("src", f64s), ref("i")), num(2)));
const body = emit([counted([scaled], length(ref("out", f64s)))]);
const fast = /^bytes\.version\.fast\d+:$[^]*?^bytes\.version\.done\d+:$/m.exec(body)![0];
// The loop keeps its integer induction, and the fast copy reads `src`
// unchecked.
expect(body).toContain("integer induction i");
expect(fast).not.toContain("bytes.index");
expect(body.slice(0, body.indexOf(fast))).toContain("bytes.index.invalid");
// A body that rebinds `src` keeps one checked loop.
const rebound = emit([
counted(
[scaled, { kind: "assign", localId: "src", value: ref("out", f64s), loc }],
length(ref("out", f64s)),
),
]);
expect(rebound).not.toContain("bytes.version.fast");
expect(rebound).toContain("bytes.index.invalid");
});
test("Float16Array accesses version in elements and share the element tag", () => {
const f16s: IrType = { kind: "bytes", elem: "f16" };
const withHalves = (body: IrStmt[]): IrFunction => {
@@ -945,12 +945,25 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
B.startBlock(done);
break;
}
const version =
const versionable =
emitter.bytesLoopVersionsEnabled &&
emitter.bytesLoopVersioning.size < MAX_BYTES_LOOP_VERSION_DEPTH &&
!emitter.bytesLoopVersioning.has(s)
? emitter.matchBytesLoopVersion(s, countedLoop)
: null;
!emitter.bytesLoopVersioning.has(s);
// A loop to a typed array's or an array's length keeps its integer
// induction. Its counted form only gives the version guard the
// counter's iteration space, so the guard also covers accesses of
// other typed arrays at the counter, such as `x[i]` and `y[i]` in
// `for (let i = 0; i < out.length; i++) out[i] = x[i] * y[i]`.
const versionCounted =
versionable && !countedLoop && integerLoop && emitter.countedLoopsEnabled
? matchIntegerCountedForLoop(
s,
emitter.numericLocals,
emitter.integerRanges,
emitter.unaliasedArrays,
)
: countedLoop;
const version = versionable ? emitter.matchBytesLoopVersion(s, versionCounted) : null;
if (version) {
const fast = B.newLabel("bytes.version.fast"),
slow = B.newLabel("bytes.version.slow"),
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6992-typed-array-length-loop-versions.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6993-typed-array-length-loop-bindings.ts"
],
"diags": []
}
@@ -0,0 +1,141 @@
// Loops bounded by one typed array's length that index other typed arrays:
// equal and different lengths, offsets, mixed element types and views that
// share a buffer. Out-of-bounds reads give undefined (NaN in arithmetic) and
// out-of-bounds writes are ignored, whichever array is shorter.
function mulInto(x: Float32Array, y: Float32Array, out: Float32Array): void {
for (let i = 0; i < out.length; i++) out[i] = x[i]! * y[i]!;
}
function axpy(a: number, x: Float64Array, y: Float64Array, out: Float64Array): void {
for (let i = 0; i < out.length; i++) out[i] = a * x[i]! + y[i]!;
}
function xorInto(a: Uint8Array, b: Uint8Array, out: Uint8Array): void {
for (let i = 0; i < out.length; i++) out[i] = a[i]! ^ b[i]!;
}
function addWrap(a: Int32Array, b: Int32Array, out: Int32Array): void {
for (let i = 0; i < out.length; i++) out[i] = (a[i]! + b[i]!) | 0;
}
function narrow(src: Float64Array, i8: Int8Array, u16: Uint16Array, u32: Uint32Array): void {
for (let i = 0; i < src.length; i++) {
i8[i] = src[i]!;
u16[i] = src[i]!;
u32[i] = src[i]!;
}
}
function neighbors(a: Int32Array, out: Int32Array): void {
for (let i = 0; i < a.length; i++) out[i] = a[i + 1]! - a[i]!;
}
function previous(a: Float64Array, out: Float64Array): void {
for (let i = 0; i < out.length; i++) out[i] = a[i - 1]!;
}
function gray(px: Uint8Array, out: Uint8Array): void {
for (let i = 0; i < out.length; i++) {
const j = i * 4;
out[i] = (px[j]! * 77 + px[j + 1]! * 150 + px[j + 2]! * 29) >> 8;
}
}
function sumPairs(a: Float64Array, b: Float64Array): number {
let s = 0;
for (let i = 0; i < a.length; i++) s += a[i]! * b[i]!;
return s;
}
function shiftUp(view: Uint8Array, from: Uint8Array): void {
for (let i = 0; i < view.length; i++) view[i] = from[i]! + 1;
}
const n = 37;
const fx = new Float32Array(n);
const fy = new Float32Array(n);
const dx = new Float64Array(n);
const dy = new Float64Array(n);
const ba = new Uint8Array(n);
const bb = new Uint8Array(n);
const ia = new Int32Array(n);
const ib = new Int32Array(n);
for (let i = 0; i < n; i++) {
fx[i] = i / 3;
fy[i] = 0.1 * i - 1.7;
dx[i] = i * 1.25 - 9;
dy[i] = (i * 7) % 11;
ba[i] = (i * 29) & 255;
bb[i] = (i * 113 + 7) & 255;
ia[i] = i * 0x3fffffff;
ib[i] = i * -0x2ffffff1;
}
const fo = new Float32Array(n);
mulInto(fx, fy, fo);
console.log("f32", fo.join(","));
const shortF = new Float32Array(5);
mulInto(fx, fy.subarray(0, 3), shortF);
console.log("f32 short input", shortF.join(","));
const longF = new Float32Array(n + 4);
mulInto(fx, fy, longF);
console.log("f32 long output", longF.subarray(n - 2).join(","));
const dout = new Float64Array(n);
axpy(-2.5, dx, dy, dout);
console.log("axpy", dout.join(","));
axpy(0.5, dx.subarray(10), dy, dout);
console.log("axpy short x", dout.subarray(20).join(","));
const bo = new Uint8Array(n);
xorInto(ba, bb, bo);
console.log("xor", bo.join(","));
const io = new Int32Array(n);
addWrap(ia, ib, io);
console.log("i32", io.join(","));
const src = new Float64Array([0, -1, 1.5, -128.5, 255.7, 256, 65535.9, -65537, 4294967296.5, NaN, Infinity, -0]);
const i8 = new Int8Array(src.length);
const u16 = new Uint16Array(src.length - 2);
const u32 = new Uint32Array(src.length + 1);
narrow(src, i8, u16, u32);
console.log("narrow", i8.join(","), "|", u16.join(","), "|", u32.join(","));
const diff = new Int32Array(n);
neighbors(ia, diff);
console.log("neighbors", diff.subarray(n - 3).join(","));
const before = new Float64Array(4);
previous(dx, before);
console.log("previous", before.join(","));
const px = new Uint8Array(n * 4);
for (let i = 0; i < px.length; i++) px[i] = (i * 53) & 255;
const g = new Uint8Array(n);
gray(px, g);
console.log("gray", g.join(","));
const wide = new Uint8Array(n + 3);
gray(px, wide);
console.log("gray past input", wide.subarray(n - 1).join(","));
console.log("dot", sumPairs(dx, dy), sumPairs(dx, dy.subarray(0, 30)));
// Views of one buffer: writes through one are visible to reads through the
// other on later iterations.
const buffer = new Uint8Array(24);
for (let i = 0; i < buffer.length; i++) buffer[i] = (i * 37) & 255;
shiftUp(buffer.subarray(1), buffer);
console.log("overlap forward", buffer.join(","));
for (let i = 0; i < buffer.length; i++) buffer[i] = (i * 37) & 255;
shiftUp(buffer, buffer.subarray(1));
console.log("overlap backward", buffer.join(","));
const shared = new ArrayBuffer(64);
const asFloat = new Float32Array(shared);
const asBytes = new Uint8Array(shared, 4, 16);
for (let i = 0; i < asFloat.length; i++) asFloat[i] = i + 0.5;
const fromBytes = new Float32Array(16);
for (let i = 0; i < fromBytes.length; i++) {
asBytes[i] = i * 3;
fromBytes[i] = asFloat[i]!;
}
console.log("mixed views", fromBytes.join(","), asBytes.join(","));
@@ -0,0 +1,146 @@
// Loops bounded by a typed array's length whose bindings change while they
// run, that leave early, or that run at module level, in generators and in
// async functions. Each keeps the reads, writes and bounds the source names
// on every iteration.
function swapBound(a: Float64Array, b: Float64Array, out: Float64Array): number {
let n = 0;
for (let i = 0; i < a.length; i++) {
out[i] = a[i]! + b[i]!;
n++;
if (i === 2) a = b;
}
return n;
}
function swapOperand(a: Int16Array, b: Int16Array, out: Int16Array): void {
for (let i = 0; i < out.length; i++) {
out[i] = a[i]! * 3;
if (i === 3) a = b;
}
}
function closureSwap(a: Uint16Array, b: Uint16Array, out: Uint16Array): void {
const swap = (): void => {
out = b;
};
for (let i = 0; i < out.length; i++) {
out[i] = a[i]! + 1000;
if (i === 1) swap();
}
}
function firstAbove(values: Float32Array, limits: Float32Array): number {
for (let i = 0; i < values.length; i++) {
if (values[i]! > limits[i]!) return i;
}
return -1;
}
function copyUntilZero(src: Uint8Array, out: Uint8Array): number {
let copied = 0;
for (let i = 0; i < src.length; i++) {
if (src[i] === 0) break;
out[i] = src[i]!;
copied++;
}
return copied;
}
function checkedScale(src: Int32Array, out: Int32Array): void {
for (let i = 0; i < src.length; i++) {
if (src[i]! < 0) throw new RangeError(`negative at ${i}: ${src[i]}`);
out[i] = src[i]! * 2;
}
}
function fromList(values: number[], out: Float64Array): void {
for (let i = 0; i < values.length; i++) {
out[i] = values[i]! / 2;
if (values.length < 6) values.push(i * 10);
}
}
function rows(table: Uint32Array[], totals: Uint32Array): void {
for (let r = 0; r < table.length; r++) {
const row = table[r]!;
let sum = 0;
for (let c = 0; c < row.length; c++) sum += row[c]! * (c + 1);
totals[r] = sum;
}
}
function* pairs(a: Int8Array, b: Int8Array): Generator<number> {
for (let i = 0; i < a.length; i++) yield a[i]! * b[i]!;
}
async function addLater(a: Float64Array, b: Float64Array, out: Float64Array): Promise<void> {
for (let i = 0; i < out.length; i++) {
out[i] = a[i]! + b[i]!;
if (i % 2 === 0) await null;
}
}
const a = new Float64Array([1, 2, 3, 4, 5, 6]);
const b = new Float64Array([10, 20, 30]);
const sum = new Float64Array(8);
console.log("swap bound", swapBound(a, b, sum), sum.join(","));
const s16 = new Int16Array([1, -2, 3, -4, 5, -6, 7]);
const t16 = new Int16Array([100, 200, 300, 400, 500, 600]);
const o16 = new Int16Array(7);
swapOperand(s16, t16, o16);
console.log("swap operand", o16.join(","));
const u = new Uint16Array([5, 6, 7, 8]);
const other = new Uint16Array(2);
const first = new Uint16Array(4);
closureSwap(u, other, first);
console.log("closure swap", first.join(","), other.join(","));
const values = new Float32Array([0.5, 1.5, 2.5, 3.5]);
console.log("first above", firstAbove(values, new Float32Array([1, 1, 1, 1])));
console.log("first above short", firstAbove(values, new Float32Array([9])));
console.log("first above none", firstAbove(values, new Float32Array([9, 9, 9, 9])));
const text = new Uint8Array([72, 105, 33, 0, 88]);
const copy = new Uint8Array(2);
console.log("copy", copyUntilZero(text, copy), copy.join(","));
const ints = new Int32Array([4, 8, -15, 16]);
const doubled = new Int32Array(4);
try {
checkedScale(ints, doubled);
} catch (error) {
console.log((error as Error).name, (error as Error).message, doubled.join(","));
}
const list = [3, 5];
const halves = new Float64Array(5);
fromList(list, halves);
console.log("from list", list.join(","), halves.join(","));
const table = [new Uint32Array([1, 2, 3]), new Uint32Array([]), new Uint32Array([4294967295, 1])];
const totals = new Uint32Array(4);
rows(table, totals);
console.log("rows", totals.join(","));
console.log("pairs", [...pairs(new Int8Array([1, -2, 3, 127]), new Int8Array([4, 5, -6]))].join(","));
const left = new Float64Array([0.25, 0.5, 0.75, 1]);
const right = new Float64Array([1, 2, 3, 4, 5]);
const later = new Float64Array(5);
const pending = addLater(left, right, later);
console.log("before await", later.join(","));
// Module-level bindings, including one a loop body reassigns.
let source = new Uint8Array([9, 8, 7, 6, 5]);
const replacement = new Uint8Array([1, 2]);
const target = new Uint8Array(5);
for (let i = 0; i < source.length; i++) {
target[i] = source[i]! + 100;
if (i === 1) source = replacement;
}
console.log("module", target.join(","), source.join(","));
pending.then(() => console.log("after await", later.join(",")));
@@ -0,0 +1 @@
{ "plain": 3800, "san": 5600 }
@@ -0,0 +1 @@
{ "plain": 2700, "san": 4000 }