mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Read a typed array's length without an invariant header load (#2225)
This commit is contained in:
@@ -9,15 +9,25 @@ import type { LlvmEmitterContext } from "./expr-context.js";
|
||||
* receiver slots loop-invariant. Untagged accesses and calls still alias
|
||||
* every category.
|
||||
*
|
||||
* Header loads are also `!invariant.load`: no store or call between the
|
||||
* construction of a view and its release changes them. Without it, a data
|
||||
* pointer load ahead of a bounds branch is sunk into the in-bounds block,
|
||||
* and LICM keeps a conditionally executed load inside the loop. */
|
||||
* Header loads that only bound or address an access to the view are also
|
||||
* `!invariant.load`: no store or call between the construction of a view
|
||||
* and its release changes them, and their values reach only that access,
|
||||
* which runs while the view is alive. Without it, a data pointer load
|
||||
* ahead of a bounds branch is sunk into the in-bounds block, and LICM keeps
|
||||
* a conditionally executed load inside the loop. */
|
||||
export function bytesHeaderAlias(host: LlvmEmitterContext): string {
|
||||
const tag = host.memoryAliasAttachment("bytes:header");
|
||||
return tag === "" ? "" : `${tag}, !invariant.load !{}`;
|
||||
}
|
||||
|
||||
/** A header load whose value the program reads (`length`, `byteLength`)
|
||||
* keeps the alias tag only. The value can outlive the view, and LLVM may
|
||||
* move an invariant load to its use, past the release that frees the
|
||||
* view. */
|
||||
export function bytesHeaderValueAlias(host: LlvmEmitterContext): string {
|
||||
return host.memoryAliasAttachment("bytes:header");
|
||||
}
|
||||
|
||||
/** Element storage's tag, and the alias scope of the access being emitted
|
||||
* in a loop version's fast copy (bytesAccessScope). */
|
||||
export function bytesElementAlias(host: LlvmEmitterContext): string {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { alwaysExactInteger, exactInteger } from "./integer-values.js";
|
||||
import { byteNumberAccess } from "../../ir/byte-numbers.js";
|
||||
import { emitByteNumber } from "./byte-numbers.js";
|
||||
import { integerOrNaNIndex, integerOrNaNRange } from "./wrapping-sums.js";
|
||||
import { bytesElementAlias, bytesHeaderAlias } from "./bytes-alias.js";
|
||||
import { bytesElementAlias, bytesHeaderAlias, bytesHeaderValueAlias } from "./bytes-alias.js";
|
||||
|
||||
const BYTES_NUM_KIND: Record<string, { kind: number; le: boolean } | undefined> = {
|
||||
u8: { kind: 0, le: false },
|
||||
@@ -160,12 +160,19 @@ export function emitBytesIndex(
|
||||
return idx;
|
||||
}
|
||||
|
||||
/** The element count of a typed array, in the target's size type. */
|
||||
export function emitBytesHeaderLength(host: LlvmEmitterContext, receiver: string): string {
|
||||
/** The element count of a typed array, in the target's size type. `value`:
|
||||
* the program reads the count itself (bytesHeaderValueAlias), rather than
|
||||
* only bounding an access. */
|
||||
export function emitBytesHeaderLength(
|
||||
host: LlvmEmitterContext,
|
||||
receiver: string,
|
||||
value = false,
|
||||
): string {
|
||||
const p = host.B.tmp();
|
||||
const len = host.B.tmp();
|
||||
const alias = value ? bytesHeaderValueAlias(host) : bytesHeaderAlias(host);
|
||||
host.B.line(`${p} = getelementptr inbounds %ScrBytes, ptr ${receiver}, i64 0, i32 1`);
|
||||
host.B.line(`${len} = load ${host.sizeType}, ptr ${p}${bytesHeaderAlias(host)}`);
|
||||
host.B.line(`${len} = load ${host.sizeType}, ptr ${p}${alias}`);
|
||||
return len;
|
||||
}
|
||||
|
||||
@@ -184,7 +191,7 @@ export function emitBytesLength(
|
||||
bytes: boolean,
|
||||
): LlValue {
|
||||
const B = host.B;
|
||||
const len = emitBytesHeaderLength(host, receiver);
|
||||
const len = emitBytesHeaderLength(host, receiver, true);
|
||||
const size = BYTES_ELEMENT_SIZE[elem];
|
||||
const count = bytes && size !== 1 ? B.tmp() : len;
|
||||
if (count !== len) B.line(`${count} = shl ${host.sizeType} ${len}, ${Math.log2(size)}`);
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile, deserializeModule, validateModule } from "../src/index.js";
|
||||
import { emitLlvmModule } from "../src/backend/llvm/emitter.js";
|
||||
import { type IrModule } from "../src/ir/ir.js";
|
||||
|
||||
async function lower(source: string): Promise<IrModule> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-bytes-header-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts"),
|
||||
output = join(dir, "main.ir.json");
|
||||
await writeFile(entry, source);
|
||||
const result = await compile(entry, { outDir: dir, outPath: output, outputKind: "ir" });
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((item) => `${item.code}: ${item.message}`).join("\n"));
|
||||
const module = deserializeModule(await readFile(output, "utf8"));
|
||||
expect(validateModule(module)).toEqual([]);
|
||||
return module;
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function body(llvm: string, symbol: string): string {
|
||||
const found = new RegExp(`^define internal [^\\n]*@${symbol}(?:\\.i32)?\\([^]*?^}`, "m").exec(
|
||||
llvm,
|
||||
);
|
||||
expect(found, symbol).not.toBeNull();
|
||||
return found![0];
|
||||
}
|
||||
|
||||
const INVARIANT_HEADER = /load (?:i64|i32|ptr), ptr [^\n]*, !tbaa !\d+, !invariant\.load !\{\}$/m;
|
||||
|
||||
test("a typed array's length read as a value is an ordinary load; access bounds stay invariant", async () => {
|
||||
const module = await lower(`
|
||||
class Table {
|
||||
slots: Int32Array = new Int32Array(4);
|
||||
swap(): number {
|
||||
const slots = this.slots;
|
||||
this.slots = new Int32Array(1);
|
||||
return slots.length + slots.byteLength;
|
||||
}
|
||||
}
|
||||
function sum(values: Float64Array, scale: number): number {
|
||||
let total = 0;
|
||||
for (let i = 0; i < values.length; i++) total += values[i]! * scale;
|
||||
return total;
|
||||
}
|
||||
const table = new Table();
|
||||
console.log(table.swap(), sum(new Float64Array([1, 2]), 2));
|
||||
`);
|
||||
const llvm = emitLlvmModule(module);
|
||||
// The length outlives the view: the release must not be crossed.
|
||||
const swap = body(llvm, "sc_bf__x25_Table_swap");
|
||||
expect(swap).toMatch(/load i64, ptr [^\n]*, !tbaa !\d+$/m);
|
||||
expect(swap).not.toContain("!invariant.load");
|
||||
expect(body(llvm, "sc_bf_sum")).toMatch(INVARIANT_HEADER);
|
||||
});
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/typed-array-length-after-release.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// A typed array's length and byteLength read just before the view's last
|
||||
// reference goes away: the values stay those of the old view after a field
|
||||
// replaces it, a map entry is deleted, or a local is rebound.
|
||||
function grow(slots: Int32Array): Int32Array {
|
||||
const next = new Int32Array(slots.length * 2);
|
||||
next.set(slots);
|
||||
return next;
|
||||
}
|
||||
|
||||
class Table {
|
||||
slots: Int32Array = new Int32Array(4);
|
||||
count = 0;
|
||||
add(value: number): number {
|
||||
const slots = this.slots;
|
||||
slots[this.count % slots.length] = value;
|
||||
this.count++;
|
||||
if (this.count === slots.length) this.slots = grow(slots);
|
||||
return this.slots.length;
|
||||
}
|
||||
swap(): number {
|
||||
const slots = this.slots;
|
||||
this.slots = new Int32Array(1);
|
||||
return slots.length;
|
||||
}
|
||||
swapBytes(): number {
|
||||
const slots = this.slots;
|
||||
const bytes = slots.byteLength;
|
||||
this.slots = new Int32Array(2);
|
||||
return bytes + slots.length;
|
||||
}
|
||||
}
|
||||
|
||||
const table = new Table();
|
||||
let size = 0;
|
||||
for (let i = 0; i < 9; i++) size = table.add(i);
|
||||
console.log(size, table.swap(), table.slots.length, table.add(5));
|
||||
console.log(table.swapBytes(), table.slots.length);
|
||||
|
||||
const views = new Map<string, Float64Array>();
|
||||
views.set("a", new Float64Array(6));
|
||||
function take(key: string): number {
|
||||
const view = views.get(key);
|
||||
if (view === undefined) return -1;
|
||||
const length = view.length;
|
||||
views.delete(key);
|
||||
return length + view.byteLength;
|
||||
}
|
||||
console.log(take("a"), take("a"), views.size);
|
||||
|
||||
let current = new Uint8Array(3);
|
||||
function rebind(): number {
|
||||
const old = current;
|
||||
current = new Uint8Array(old.length + 1);
|
||||
return old.length * 10 + current.length;
|
||||
}
|
||||
console.log(rebind(), rebind(), current.length);
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2800, "san": 2500 }
|
||||
Reference in New Issue
Block a user