mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-29 07:04:09 +08:00
Convert clamped stores inline and test thread-owned views once in typed-array loops (#1767)
This commit is contained in:
@@ -26,7 +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-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, `Uint8ClampedArray` brighten, 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 |
|
||||
|
||||
@@ -17,6 +17,10 @@ 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 brighten(src: Uint8ClampedArray, gain: number, out: Uint8ClampedArray): void {
|
||||
for (let i = 0; i < out.length; i++) out[i] = src[i]! * gain + 8;
|
||||
}
|
||||
|
||||
function gray(rgba: Uint8Array, out: Uint8Array): void {
|
||||
for (let i = 0; i < out.length; i++) {
|
||||
const j = i * 4;
|
||||
@@ -38,6 +42,8 @@ const masked = new Uint8Array(n);
|
||||
const ia = new Int32Array(n);
|
||||
const ib = new Int32Array(n);
|
||||
const io = new Int32Array(n);
|
||||
const pixels = new Uint8ClampedArray(n);
|
||||
const bright = new Uint8ClampedArray(n);
|
||||
const rgba = new Uint8Array(n * 4);
|
||||
const luma = new Uint8Array(n);
|
||||
let state = 2463534242;
|
||||
@@ -56,6 +62,7 @@ for (let i = 0; i < n; i++) {
|
||||
key[i] = next() & 255;
|
||||
ia[i] = next() | 0;
|
||||
ib[i] = next() | 0;
|
||||
pixels[i] = next() & 255;
|
||||
}
|
||||
for (let i = 0; i < rgba.length; i++) rgba[i] = next() & 255;
|
||||
|
||||
@@ -64,6 +71,7 @@ let f64Sum = 0;
|
||||
let f32Sum = 0;
|
||||
let byteSum = 0;
|
||||
let intSum = 0;
|
||||
let brightSum = 0;
|
||||
let lumaSum = 0;
|
||||
for (let r = 0; r < rounds; r++) {
|
||||
// Change one input element per round so no round repeats the last.
|
||||
@@ -72,21 +80,25 @@ for (let r = 0; r < rounds; r++) {
|
||||
fa[at] = r;
|
||||
data[at] = r & 255;
|
||||
ia[at] = r * 65537;
|
||||
pixels[at] = r & 255;
|
||||
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);
|
||||
brighten(pixels, 0.75 + (r & 7) / 8, bright);
|
||||
gray(rgba, luma);
|
||||
const probe = (r * 104729) & (n - 1);
|
||||
f64Sum += d[probe]!;
|
||||
f32Sum += fo[probe]!;
|
||||
byteSum += masked[probe]!;
|
||||
intSum = (intSum + io[probe]!) | 0;
|
||||
brightSum += bright[probe]!;
|
||||
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(`clamped ${brightSum}`);
|
||||
console.log(`gray ${lumaSum}`);
|
||||
|
||||
@@ -109,6 +109,8 @@ function match(
|
||||
ranges,
|
||||
initialized: findInitializedByteLoopBindings(f).get(loop) ?? new Set(),
|
||||
proven: new Set(),
|
||||
workers: false,
|
||||
owned: new Set(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -250,6 +252,35 @@ test("loops to a typed array's length version the other typed arrays they index"
|
||||
expect(rebound).toContain("bytes.index.invalid");
|
||||
});
|
||||
|
||||
test("worker programs test each receiver's shared storage once in the guard", () => {
|
||||
// for (let i = 0; i < n; i++) out[i] = src[i] * 2, where either view may
|
||||
// be shared.
|
||||
const loop = counted([
|
||||
set(ref("out", f64s), ref("i"), bin("*", get(ref("src", f64s), ref("i")), num(2))),
|
||||
]);
|
||||
const kernel = fn([loop]);
|
||||
const mod: IrModule = {
|
||||
irVersion: 17,
|
||||
sourceFile: loc.file,
|
||||
entry: "__main",
|
||||
workers: true,
|
||||
functions: [
|
||||
{ name: "__main", params: [], returnType: VOID, locals: [], body: [], loc },
|
||||
kernel,
|
||||
],
|
||||
};
|
||||
const body = /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(emitLlvmModule(mod))![0];
|
||||
const guard = body.slice(0, body.search(/^bytes\.version\.(fast|slow)\d+:$/m));
|
||||
expect(guard.match(/icmp eq ptr %\S+, null/g)).toHaveLength(2);
|
||||
const fast = /^bytes\.version\.fast\d+:$[^]*?^bytes\.version\.done\d+:$/m.exec(body)![0];
|
||||
expect(fast).not.toContain("bytes.shared");
|
||||
expect(fast).not.toMatch(/(load|store) atomic double/);
|
||||
// The checked copy keeps the shared-memory path for each access.
|
||||
const slow = body.slice(body.search(/^bytes\.version\.slow\d+:$/m), body.indexOf(fast));
|
||||
expect(slow).toContain("load atomic double");
|
||||
expect(slow).toContain("store atomic double");
|
||||
});
|
||||
|
||||
test("Float16Array accesses version in elements and share the element tag", () => {
|
||||
const f16s: IrType = { kind: "bytes", elem: "f16" };
|
||||
const withHalves = (body: IrStmt[]): IrFunction => {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { byteNumberAccess } from "../../ir/byte-numbers.js";
|
||||
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
|
||||
import { exactInteger, widenInteger } from "./integer-values.js";
|
||||
import { emitCountedLoopLimit, emitCountedLoopSafety } from "./counted-loops.js";
|
||||
import { emitBytesHeaderLength } from "./expr-bytes.js";
|
||||
import { emitBytesHeaderLength, emitBytesSharedStorage } from "./expr-bytes.js";
|
||||
|
||||
type Node = IrExpr | IrStmt;
|
||||
type VarRef = IrExpr & { kind: "varRef" };
|
||||
@@ -41,8 +41,13 @@ interface AffineCheck {
|
||||
export interface BytesLoopVersion {
|
||||
/** Typed-array accesses that are in bounds whenever the guard holds. */
|
||||
bounds: ReadonlySet<Node>;
|
||||
/** Worker programs: typed-array accesses whose views the guard proves
|
||||
* thread-owned (no shared storage). */
|
||||
owned: ReadonlySet<Node>;
|
||||
/** Root receiver bindings, read once before the loop. */
|
||||
receivers: { ref: VarRef; global: boolean }[];
|
||||
/** Receivers the guard requires to have no shared storage. */
|
||||
ownedReceivers: number[];
|
||||
fixed: FixedCheck[];
|
||||
affine: AffineCheck[];
|
||||
/** The counted loop whose iteration space the affine checks cover. */
|
||||
@@ -66,6 +71,11 @@ export interface BytesLoopFacts {
|
||||
initialized: ReadonlySet<string>;
|
||||
/** Accesses already proved in bounds without a guard. */
|
||||
proven: ReadonlySet<Node>;
|
||||
/** Worker programs: an access of a view that may be shared tests the
|
||||
* view's shared storage first. */
|
||||
workers: boolean;
|
||||
/** Accesses already proved thread-owned by an enclosing version. */
|
||||
owned: ReadonlySet<Node>;
|
||||
}
|
||||
|
||||
const MAX_COST = 400;
|
||||
@@ -87,6 +97,11 @@ const INVARIANT_OPS = new Set(["+", "-", "*", "&", "|", "^", "<<", ">>", ">>>"])
|
||||
* and last values in bounds, such as `out[base + i]` for `i < n` or
|
||||
* `view.getUint32(i * 4)`.
|
||||
*
|
||||
* In worker programs, every access of a view that may be shared tests its
|
||||
* shared storage. A view's storage is also fixed at construction, so the
|
||||
* same guard tests each invariant receiver once, and the fast copy reads
|
||||
* and writes thread-owned views directly.
|
||||
*
|
||||
* Only small synchronous loops are versioned (nested loops included), and
|
||||
* the emitter bounds how deeply versions nest. The checked loop runs
|
||||
* whenever the guard fails, and keeps every out-of-bounds read, ignored
|
||||
@@ -314,10 +329,19 @@ export function matchBytesLoopVersion(
|
||||
}
|
||||
return index;
|
||||
};
|
||||
// Worker programs: accesses through invariant receivers, proven or not,
|
||||
// become thread-owned when the guard finds no shared storage.
|
||||
const owned = new Map<number, Node[]>();
|
||||
const access = (node: Node, array: IrExpr, index: IrExpr, width: number): void => {
|
||||
if (facts.proven.has(node)) return;
|
||||
const ref = root(array);
|
||||
if (!ref) return;
|
||||
if (facts.workers && !facts.owned.has(node)) {
|
||||
const receiver = receiverOf(ref);
|
||||
const nodes = owned.get(receiver);
|
||||
if (nodes) nodes.push(node);
|
||||
else owned.set(receiver, [node]);
|
||||
}
|
||||
if (facts.proven.has(node)) return;
|
||||
const shape = counter !== null ? affine(index) : null;
|
||||
if (shape && shape.counter !== 0) {
|
||||
const operands = new Set<IrExpr>();
|
||||
@@ -383,9 +407,15 @@ export function matchBytesLoopVersion(
|
||||
});
|
||||
const fixedKept = [...fixed.values()].slice(0, MAX_CHECKS);
|
||||
const affineKept = affineGroups.slice(0, MAX_CHECKS - fixedKept.length);
|
||||
if (fixedKept.length === 0 && affineKept.length === 0) return null;
|
||||
const ownedKept = [...owned.entries()].slice(0, MAX_CHECKS);
|
||||
if (fixedKept.length === 0 && affineKept.length === 0 && ownedKept.length === 0) return null;
|
||||
const bounds = new Set<Node>();
|
||||
const ownedNodes = new Set<Node>();
|
||||
const used = new Set<number>();
|
||||
for (const [receiver, nodes] of ownedKept) {
|
||||
used.add(receiver);
|
||||
for (const node of nodes) ownedNodes.add(node);
|
||||
}
|
||||
const ranges = new Map<IrExpr, IntegerRange>();
|
||||
const counterLow = Math.max(counted?.range.min ?? 0, -bound),
|
||||
counterHigh = Math.min(counted?.range.max ?? 0, bound);
|
||||
@@ -420,7 +450,9 @@ export function matchBytesLoopVersion(
|
||||
(counted.range.min < -bound || counted.range.max > bound);
|
||||
return {
|
||||
bounds,
|
||||
owned: ownedNodes,
|
||||
receivers: order.map((index) => receivers[index]!),
|
||||
ownedReceivers: ownedKept.map(([receiver]) => renumber.get(receiver)!),
|
||||
fixed: fixedKept.map((g) => ({
|
||||
receiver: renumber.get(g.check.receiver)!,
|
||||
end: g.check.end,
|
||||
@@ -439,7 +471,8 @@ export function matchBytesLoopVersion(
|
||||
}
|
||||
|
||||
/** Branches to `fast` when every access the version proves is in bounds for
|
||||
* every iteration, and to `slow` otherwise (including a counted loop whose
|
||||
* every iteration and every view it proves thread-owned has no shared
|
||||
* storage, and to `slow` otherwise (including a counted loop whose
|
||||
* integer form is not established, or a module receiver not yet set). */
|
||||
export function emitBytesLoopVersionGuard(
|
||||
host: LlvmEmitterContext,
|
||||
@@ -473,6 +506,11 @@ export function emitBytesLoopVersionGuard(
|
||||
return wide;
|
||||
});
|
||||
const conditions: string[] = [];
|
||||
for (const index of version.ownedReceivers) {
|
||||
const owned = B.tmp();
|
||||
B.line(`${owned} = icmp eq ptr ${emitBytesSharedStorage(host, receivers[index]!)}, null`);
|
||||
conditions.push(owned);
|
||||
}
|
||||
for (const check of version.fixed) {
|
||||
const ok = B.tmp();
|
||||
B.line(`${ok} = icmp ugt i64 ${lengths[check.receiver]}, ${check.end}`);
|
||||
|
||||
@@ -228,6 +228,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
emitter.integerLoopRanges = rangeFacts.loopRanges;
|
||||
emitter.int32Elements = slotFacts.elements ?? null;
|
||||
emitter.bytesBounds = findBytesBounds(numericFn, emitter.integerRanges);
|
||||
emitter.bytesOwned = new Set();
|
||||
// Debug builds keep every number local a double for the debugger's
|
||||
// variable view. Lazily boxed captured parameters keep their double,
|
||||
// which the box is created from, and so do plain copies of a capture or
|
||||
|
||||
@@ -517,6 +517,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
s.index,
|
||||
s.value,
|
||||
emitter.bytesBounds.has(s),
|
||||
emitter.bytesOwned.has(s),
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -970,6 +971,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
done = B.newLabel("bytes.version.done");
|
||||
emitBytesLoopVersionGuard(emitter, version, fast, slow);
|
||||
const originalBounds = emitter.bytesBounds,
|
||||
originalOwned = emitter.bytesOwned,
|
||||
originalRanges = emitter.integerRanges;
|
||||
emitter.bytesLoopVersioning.add(s);
|
||||
try {
|
||||
@@ -978,6 +980,8 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
B.br(done);
|
||||
B.startBlock(fast);
|
||||
emitter.bytesBounds = new Set([...originalBounds, ...version.bounds]);
|
||||
if (version.owned.size > 0)
|
||||
emitter.bytesOwned = new Set([...originalOwned, ...version.owned]);
|
||||
if (version.ranges.size > 0)
|
||||
emitter.integerRanges = new Map([...originalRanges, ...version.ranges]);
|
||||
if (version.counted?.guarded) emitter.provenCountedGuards.add(s);
|
||||
@@ -985,6 +989,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
B.br(done);
|
||||
} finally {
|
||||
emitter.bytesBounds = originalBounds;
|
||||
emitter.bytesOwned = originalOwned;
|
||||
emitter.integerRanges = originalRanges;
|
||||
emitter.provenCountedGuards.delete(s);
|
||||
emitter.bytesLoopVersioning.delete(s);
|
||||
|
||||
@@ -1125,6 +1125,9 @@ export class LlEmitter {
|
||||
* guard already passed. */
|
||||
readonly provenCountedGuards = new Set<IrStmt>();
|
||||
bytesBounds: ReadonlySet<IrExpr | IrStmt> = new Set();
|
||||
/** Worker programs: typed-array accesses whose views an enclosing version
|
||||
* guard proved thread-owned. */
|
||||
bytesOwned: ReadonlySet<IrExpr | IrStmt> = new Set();
|
||||
integerViews = new Map<string, string>();
|
||||
/** Module globals the current function owns outright (initializer
|
||||
* bindings): their function-local slot, which stands in for the global. */
|
||||
@@ -3836,6 +3839,8 @@ export class LlEmitter {
|
||||
ranges: this.integerRanges,
|
||||
initialized,
|
||||
proven: this.bytesBounds,
|
||||
workers: this.mod.workers === true,
|
||||
owned: this.bytesOwned,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6196,8 +6201,9 @@ export class LlEmitter {
|
||||
expr?: IrExpr,
|
||||
inBounds = false,
|
||||
invalidNaN = false,
|
||||
owned = false,
|
||||
): LlValue {
|
||||
return emitBytesGet(this, elem, receiver, index, expr, inBounds, invalidNaN);
|
||||
return emitBytesGet(this, elem, receiver, index, expr, inBounds, invalidNaN, owned);
|
||||
}
|
||||
|
||||
emitToUint32(value: string, expr?: IrExpr, uint32?: string): string {
|
||||
@@ -6260,8 +6266,9 @@ export class LlEmitter {
|
||||
indexExpr?: IrExpr,
|
||||
expr?: IrExpr,
|
||||
inBounds = false,
|
||||
owned = false,
|
||||
): void {
|
||||
return emitBytesSet(this, elem, receiver, index, value, indexExpr, expr, inBounds);
|
||||
return emitBytesSet(this, elem, receiver, index, value, indexExpr, expr, inBounds, owned);
|
||||
}
|
||||
|
||||
emitBytesIntrinsic(e: IrExpr & { kind: "bytesIntrinsic" }): LlValue {
|
||||
|
||||
@@ -176,6 +176,17 @@ export function emitBytesLength(
|
||||
return { name: out, type: F64 };
|
||||
}
|
||||
|
||||
/** A view's shared backing (null for a buffer this thread owns). It is
|
||||
* set at construction, before script code can see the view. */
|
||||
export function emitBytesSharedStorage(host: LlvmEmitterContext, receiver: string): string {
|
||||
const B = host.B;
|
||||
const slot = B.tmp(),
|
||||
storage = B.tmp();
|
||||
B.line(`${slot} = getelementptr inbounds %ScrBytes, ptr ${receiver}, i64 0, i32 9`);
|
||||
B.line(`${storage} = load ptr, ptr ${slot}${bytesHeaderAlias(host)}`);
|
||||
return storage;
|
||||
}
|
||||
|
||||
/** Worker programs: an element of a view of a SharedArrayBuffer (`shared` is
|
||||
* fixed before script code can see the view) is read and written with one
|
||||
* relaxed atomic access of its width, as the runtime's shared accessors do;
|
||||
@@ -186,12 +197,9 @@ export function emitBytesLength(
|
||||
function emitSharedBytesSplit(host: LlvmEmitterContext, receiver: string): string {
|
||||
const B = host.B;
|
||||
host.declare(`declare i1 @llvm.expect.i1(i1, i1)`);
|
||||
const slot = B.tmp(),
|
||||
storage = B.tmp(),
|
||||
const storage = emitBytesSharedStorage(host, receiver),
|
||||
isShared = B.tmp(),
|
||||
expected = B.tmp();
|
||||
B.line(`${slot} = getelementptr inbounds %ScrBytes, ptr ${receiver}, i64 0, i32 9`);
|
||||
B.line(`${storage} = load ptr, ptr ${slot}${bytesHeaderAlias(host)}`);
|
||||
B.line(`${isShared} = icmp ne ptr ${storage}, null`);
|
||||
B.line(`${expected} = call i1 @llvm.expect.i1(i1 ${isShared}, i1 false)`);
|
||||
const shared = B.newLabel("bytes.shared"),
|
||||
@@ -209,15 +217,17 @@ export function emitBytesGet(
|
||||
expr?: IrExpr,
|
||||
inBounds = false,
|
||||
invalidNaN = false,
|
||||
owned = false,
|
||||
): LlValue {
|
||||
const B = host.B;
|
||||
// Any invalid index traps (the array runtime's discipline).
|
||||
if (!invalidNaN) return emitBytesLoad(host, elem, receiver, index, expr, inBounds, undefined);
|
||||
if (!invalidNaN)
|
||||
return emitBytesLoad(host, elem, receiver, index, expr, inBounds, undefined, owned);
|
||||
// The numeric view of an ordinary read: an invalid index never touches
|
||||
// storage and reads as NaN — ToNumber of Node's undefined.
|
||||
const invalid = B.newLabel("bytes.read.invalid");
|
||||
const done = B.newLabel("bytes.read.done");
|
||||
const loaded = emitBytesLoad(host, elem, receiver, index, expr, inBounds, invalid);
|
||||
const loaded = emitBytesLoad(host, elem, receiver, index, expr, inBounds, invalid, owned);
|
||||
const ok = B.newLabel("bytes.read.ok");
|
||||
B.br(ok);
|
||||
B.startBlock(ok);
|
||||
@@ -260,8 +270,10 @@ function emitBytesLoad(
|
||||
expr: IrExpr | undefined,
|
||||
inBounds: boolean,
|
||||
invalid: string | undefined,
|
||||
owned: boolean,
|
||||
): LlValue {
|
||||
if (host.mod.workers) {
|
||||
// `owned`: an enclosing version guard proved the view thread-owned.
|
||||
if (host.mod.workers && !owned) {
|
||||
const idx = emitBytesIndex(host, receiver, index, expr, invalid, inBounds);
|
||||
return emitSharedOrLocalLoad(host, elem, receiver, idx);
|
||||
}
|
||||
@@ -515,13 +527,14 @@ export function emitBytesSet(
|
||||
indexExpr?: IrExpr,
|
||||
expr?: IrExpr,
|
||||
inBounds = false,
|
||||
owned = false,
|
||||
): void {
|
||||
const B = host.B;
|
||||
// Invalid integer-indexed writes are ignored after evaluating the RHS.
|
||||
const done = B.newLabel("bytes.store.done");
|
||||
const data = host.emitBytesData(receiver);
|
||||
const idx = emitBytesIndex(host, receiver, index, indexExpr, done, inBounds);
|
||||
if (host.mod.workers && elem === "f16") {
|
||||
if (host.mod.workers && !owned && elem === "f16") {
|
||||
host.declare(`declare void @scr_bytes_shared_set(ptr, ${host.sizeType}, double)`);
|
||||
const local = emitSharedBytesSplit(host, receiver);
|
||||
B.line(
|
||||
@@ -532,10 +545,24 @@ export function emitBytesSet(
|
||||
}
|
||||
let stored: string | null;
|
||||
if (elem === "u8c") {
|
||||
host.declare(`declare double @scr_bytes_to_u8_clamp(double)`);
|
||||
const clamped = B.tmp();
|
||||
// ToUint8Clamp, as scr_bytes_to_u8_clamp computes it: NaN and values
|
||||
// at most 0 store 0, values at least 255 store 255, and values between
|
||||
// round to the nearest integer, ties to even. Adding and subtracting
|
||||
// 2^52 rounds a value in (0, 255) that way in the default rounding mode,
|
||||
// with no call or branch, so loops of clamped stores can vectorize.
|
||||
const positive = B.tmp(),
|
||||
high = B.tmp(),
|
||||
shifted = B.tmp(),
|
||||
rounded = B.tmp(),
|
||||
upper = B.tmp(),
|
||||
clamped = B.tmp();
|
||||
stored = B.tmp();
|
||||
B.line(`${clamped} = call double @scr_bytes_to_u8_clamp(double ${value.name})`);
|
||||
B.line(`${positive} = fcmp ogt double ${value.name}, 0.0`);
|
||||
B.line(`${high} = fcmp oge double ${value.name}, 255.0`);
|
||||
B.line(`${shifted} = fadd double ${value.name}, 0x4330000000000000`);
|
||||
B.line(`${rounded} = fsub double ${shifted}, 0x4330000000000000`);
|
||||
B.line(`${upper} = select i1 ${high}, double 255.0, double ${rounded}`);
|
||||
B.line(`${clamped} = select i1 ${positive}, double ${upper}, double 0.0`);
|
||||
B.line(`${stored} = fptoui double ${clamped} to i32`);
|
||||
} else
|
||||
stored =
|
||||
@@ -572,7 +599,7 @@ export function emitBytesSet(
|
||||
ty = "i32";
|
||||
operand = stored!;
|
||||
}
|
||||
if (host.mod.workers) {
|
||||
if (host.mod.workers && !owned) {
|
||||
// A shared view's element takes a relaxed atomic store of its width.
|
||||
const local = emitSharedBytesSplit(host, receiver);
|
||||
const p = B.tmp();
|
||||
@@ -721,6 +748,7 @@ export function emitBytesIntrinsic(
|
||||
e.args[0],
|
||||
host.bytesBounds.has(e),
|
||||
e.invalidNaN === true,
|
||||
host.bytesOwned.has(e),
|
||||
);
|
||||
case "slice":
|
||||
return call(
|
||||
|
||||
@@ -406,3 +406,45 @@ test("large record clones stay outlined while small clones remain inlineable", (
|
||||
expect(large).toContain("define internal ptr @sc_rclone_large(ptr %src) #2");
|
||||
expect(large).toContain("attributes #2 = { noinline sanitize_address }");
|
||||
});
|
||||
|
||||
test("Uint8ClampedArray stores clamp and round without a runtime call", () => {
|
||||
const type = bytesOf("u8c");
|
||||
const mod: IrModule = {
|
||||
irVersion: 17,
|
||||
sourceFile: loc.file,
|
||||
entry: "__main",
|
||||
functions: [
|
||||
{ name: "__main", params: [], returnType: VOID, locals: [], body: [], loc },
|
||||
{
|
||||
name: "store",
|
||||
params: [
|
||||
{ localId: "px", name: "px", type },
|
||||
{ localId: "v", name: "v", type: F64 },
|
||||
],
|
||||
returnType: VOID,
|
||||
locals: [
|
||||
{ id: "px", name: "px", type, mutable: false },
|
||||
{ id: "v", name: "v", type: F64, mutable: false },
|
||||
],
|
||||
body: [
|
||||
{
|
||||
kind: "bytesSet",
|
||||
arr: { kind: "varRef", localId: "px", type, loc },
|
||||
index: { kind: "numLit", value: 0, type: F64, loc },
|
||||
value: { kind: "varRef", localId: "v", type: F64, loc },
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(validateModule(mod)).toEqual([]);
|
||||
const body = /^define internal [^\n]*@sc_bf_store[^]*?^}/m.exec(emitLlvmModule(mod))![0];
|
||||
expect(body).not.toContain("scr_bytes_to_u8_clamp");
|
||||
expect(body).toMatch(/fcmp ogt double %\S+, 0\.0/);
|
||||
expect(body).toMatch(/fcmp oge double %\S+, 255\.0/);
|
||||
expect(body).toMatch(/fadd double %\S+, 0x4330000000000000/);
|
||||
expect(body).toMatch(/fsub double %\S+, 0x4330000000000000/);
|
||||
expect(body).toMatch(/store i8 [^\n]*, align 1/);
|
||||
});
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6994-uint8-clamped-store-rounding.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6995-worker-typed-array-loop-versions.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Uint8ClampedArray stores: NaN and values at most 0 store 0, values at
|
||||
// least 255 store 255, and values between round to the nearest integer,
|
||||
// ties to even. Covers single stores and loops, every tie between 0 and
|
||||
// 255 with the doubles on either side of it, and values far outside.
|
||||
|
||||
/** The largest power of two at most `x` (x > 0). */
|
||||
function floorPower(x: number): number {
|
||||
let p = 1;
|
||||
while (p * 2 <= x) p *= 2;
|
||||
while (p > x) p /= 2;
|
||||
return p;
|
||||
}
|
||||
|
||||
/** The doubles just above and below `x` (x >= 0). */
|
||||
function nextUp(x: number): number {
|
||||
return x === 0 ? 5e-324 : x + floorPower(x) * 2 ** -52;
|
||||
}
|
||||
|
||||
function nextDown(x: number): number {
|
||||
if (x === 0) return -5e-324;
|
||||
const p = floorPower(x);
|
||||
return x - (p === x ? p * 2 ** -53 : p * 2 ** -52);
|
||||
}
|
||||
|
||||
function storeAll(values: Float64Array, out: Uint8ClampedArray): void {
|
||||
const n = out.length;
|
||||
for (let i = 0; i < n; i++) out[i] = values[i]!;
|
||||
}
|
||||
|
||||
function scale(px: Uint8ClampedArray, k: number): void {
|
||||
const n = px.length;
|
||||
for (let i = 0; i < n; i++) px[i] = px[i]! * k;
|
||||
}
|
||||
|
||||
const special = [
|
||||
NaN, -NaN, Infinity, -Infinity, 0, -0, 5e-324, -5e-324, 2.2250738585072014e-308, 0.49999999999999994,
|
||||
0.5, 0.5000000000000001, 1, 1.5, 2.5, 3.5, 127.5, 128.5, 253.5, 254.49999999999997, 254.5,
|
||||
254.50000000000003, 254.99999999999997, 255, 255.00000000000003, 255.5, 256, -1, -0.5, -0.4,
|
||||
1e300, -1e300, 2 ** 31, 2 ** 32 + 0.5, 2 ** 52, 2 ** 52 + 1, 2 ** 53, -(2 ** 52),
|
||||
];
|
||||
const one = new Uint8ClampedArray(1);
|
||||
const singles: number[] = [];
|
||||
for (const v of special) {
|
||||
one[0] = v;
|
||||
singles.push(one[0]!);
|
||||
}
|
||||
console.log("single", singles.join(","));
|
||||
|
||||
const ties: number[] = [];
|
||||
for (let k = 0; k <= 255; k++) {
|
||||
ties.push(k + 0.5, nextDown(k + 0.5), nextUp(k + 0.5), k, nextDown(k), nextUp(k));
|
||||
}
|
||||
const values = new Float64Array(ties.length + special.length);
|
||||
values.set(ties);
|
||||
values.set(special, ties.length);
|
||||
const stored = new Uint8ClampedArray(values.length);
|
||||
storeAll(values, stored);
|
||||
const expected = new Uint8ClampedArray(values.length);
|
||||
for (let i = 0; i < values.length; i++) expected[i] = values[i]!;
|
||||
let mismatches = 0;
|
||||
for (let i = 0; i < values.length; i++) if (stored[i] !== expected[i]) mismatches++;
|
||||
console.log("loop", values.length, "mismatches", mismatches);
|
||||
let digest = 0;
|
||||
for (let i = 0; i < stored.length; i++) digest = (Math.imul(digest, 31) + stored[i]!) | 0;
|
||||
console.log("digest", digest, stored.subarray(0, 24).join(","));
|
||||
console.log("tail", stored.subarray(ties.length).join(","));
|
||||
|
||||
const px = new Uint8ClampedArray(64);
|
||||
for (let i = 0; i < px.length; i++) px[i] = i * 4.25;
|
||||
scale(px, 1.37);
|
||||
console.log("scaled", px.join(","));
|
||||
scale(px, -1);
|
||||
console.log("negated", px.join(","));
|
||||
const fromInts = new Uint8ClampedArray(8);
|
||||
const ints = new Int32Array([-300, -1, 0, 1, 254, 255, 256, 2147483647]);
|
||||
for (let i = 0; i < fromInts.length; i++) fromInts[i] = ints[i]!;
|
||||
console.log("ints", fromInts.join(","));
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
|
||||
|
||||
// Typed-array loops in a worker-enabled program, over views of a thread's
|
||||
// own ArrayBuffer, views of a SharedArrayBuffer, and both in one loop. Each
|
||||
// loop gives the same results whichever kind of buffer its views use, and
|
||||
// writes through shared views are visible to the other thread.
|
||||
|
||||
function scale(src: Float64Array, dst: Float32Array, k: number): void {
|
||||
for (let i = 0; i < dst.length; i++) dst[i] = src[i]! * k;
|
||||
}
|
||||
|
||||
function countAbove(view: Int16Array, limit: number): number {
|
||||
let n = 0;
|
||||
for (let i = 0; i < view.length; i++) if (view[i]! > limit) n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
function mix(a: Uint8Array, b: Uint8Array, out: Uint8ClampedArray): void {
|
||||
const n = out.length;
|
||||
for (let i = 0; i < n; i++) out[i] = a[i]! * 2 - b[i]!;
|
||||
}
|
||||
|
||||
function bump(view: Int32Array, by: number): void {
|
||||
for (let i = 0; i < view.length; i++) view[i] = (view[i]! + by) | 0;
|
||||
}
|
||||
|
||||
function report(label: string, src: Float64Array, dst: Float32Array, ints: Int16Array, a: Uint8Array, b: Uint8Array, out: Uint8ClampedArray): string {
|
||||
for (let i = 0; i < src.length; i++) src[i] = i * 1.5 - 4;
|
||||
for (let i = 0; i < ints.length; i++) ints[i] = (i * 7919) % 2001 - 1000;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
a[i] = i * 37;
|
||||
b[i] = 255 - i * 11;
|
||||
}
|
||||
scale(src, dst, 0.1);
|
||||
mix(a, b, out);
|
||||
return `${label}: ${dst.join(",")} | ${countAbove(ints, 250)} ${countAbove(ints.subarray(3), -1)} | ${out.join(",")}`;
|
||||
}
|
||||
|
||||
function views(buffer: ArrayBuffer | SharedArrayBuffer, other: ArrayBuffer | SharedArrayBuffer): string {
|
||||
const src = new Float64Array(buffer, 0, 12);
|
||||
const dst = new Float32Array(other, 0, 10);
|
||||
const ints = new Int16Array(buffer, 96, 40);
|
||||
const a = new Uint8Array(other, 40, 16);
|
||||
const b = new Uint8Array(buffer, 176, 16);
|
||||
const out = new Uint8ClampedArray(other, 56, 16);
|
||||
return report("pair", src, dst, ints, a, b, out);
|
||||
}
|
||||
|
||||
if (isMainThread) {
|
||||
// Bytes 224-255 of the shared buffer hold counters the views never use.
|
||||
const shared = new SharedArrayBuffer(256);
|
||||
console.log(views(new ArrayBuffer(256), new ArrayBuffer(256)));
|
||||
console.log(views(shared, new ArrayBuffer(256)));
|
||||
console.log(views(new ArrayBuffer(256), shared));
|
||||
console.log(views(shared, new SharedArrayBuffer(256)));
|
||||
const own = new Int32Array(8);
|
||||
bump(own, 5);
|
||||
bump(new Int32Array(shared, 224, 8), 1);
|
||||
const worker = new Worker(new URL(import.meta.url), { workerData: shared });
|
||||
worker.on("message", (line: string) => console.log(line));
|
||||
worker.on("exit", (code: number) => {
|
||||
const seen = new Int32Array(shared, 224, 8);
|
||||
bump(seen, 1000);
|
||||
console.log(`exit ${code}: ${seen.join(",")} own ${own.join(",")}`);
|
||||
console.log(`dst after worker: ${new Float32Array(shared, 0, 10).join(",")}`);
|
||||
});
|
||||
} else {
|
||||
const shared = workerData as SharedArrayBuffer;
|
||||
const counters = new Int32Array(shared, 224, 8);
|
||||
bump(counters, 10);
|
||||
bump(counters.subarray(2, 5), -3);
|
||||
parentPort!.postMessage(`worker: ${counters.join(",")}`);
|
||||
parentPort!.postMessage(views(new ArrayBuffer(256), shared));
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2400, "san": 3300 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2700, "san": 3900 }
|
||||
Reference in New Issue
Block a user