mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
ScrArr stores len and cap as 32-bit fields and no longer stores the state pointer. The dense states share the values' allocation and run backward from its end: the state of slot i is at data + cap * (slot width + 1) - 1 - i. Removing slots from the front (shift, splice(0, n)) advances data and lowers cap, which leaves every remaining state at its address; the head room size, needed only to free or reallocate the block, is kept in the vacated slot before data. On 64-bit targets the header goes from 64 to 48 bytes, and from 36 to 32 bytes on wasm32. Array lengths were already limited to 2^32 - 1. The emitted code loads len and cap as i32 and zero-extends them, and computes state addresses from data and cap. Runtime loops compute the end of the states once per call. Local array reads load data once for both the state and value paths. A local or strict element read whose index is a loop's induction variable reads slots above the hole-free prefix through the runtime: an inline state check there makes LLVM keep -1 - i as a second induction variable, which costs a register in the loop. Bulk operations do not read back states they just wrote: an append to an array whose hole-free prefix reaches its length moves the prefix bound to the new length, a copy from below the source's prefix writes value states without reading the source's, and releasing a string array skips the state checks below the prefix. Copies of up to eight slots and of short state runs use overlapping word moves instead of a call. Emitted element access computes the state address only on the state-checked path. Without these, programs that build many small arrays with split or slice ran 2-4% slower than with the separate state pointer. Integer loops over an array's length compare the counter with the i32 length field in 32 bits; the counter never exceeds 2^32 - 1 there. The array-length stability proof accepts Map and Set reads (get, has, size) and checked union extractions (`x!`), which run no user code, so loops that look up maps keep the dense fast copy, which tests the hole-free prefix once before the loop. Small constructions with the small allocator request 6 slots, so their blocks stay 112 bytes, now with 7 inline slots (6 after a collector header). With 96-byte blocks of 5 slots, programs that free and reuse many small arrays ran 15-20% slower. RUNTIME_ABI_VERSION goes from 22 to 23.
95 lines
3.3 KiB
TypeScript
95 lines
3.3 KiB
TypeScript
// Array lengths and indices at the 2^32 - 1 limit, sparse writes past the
|
|
// dense capacity, holes and boolean arrays: the length and capacity fit in
|
|
// 32 bits, and the dense states follow the value slots.
|
|
|
|
function report(label: string, run: () => unknown): void {
|
|
try {
|
|
console.log(label, run());
|
|
} catch (e) {
|
|
console.log(label, e instanceof Error ? `${e.name}: ${e.message}` : e);
|
|
}
|
|
}
|
|
|
|
const big: string[] = [];
|
|
big.length = 4294967295;
|
|
console.log("max length", big.length);
|
|
report("length 2^32", () => {
|
|
const a: number[] = [];
|
|
a.length = 4294967296;
|
|
return a.length;
|
|
});
|
|
report("length -1", () => {
|
|
const a: number[] = [];
|
|
a.length = -1;
|
|
return a.length;
|
|
});
|
|
|
|
const edge: string[] = [];
|
|
edge[4294967294] = "last";
|
|
console.log("max index", edge.length, edge[4294967294]);
|
|
|
|
// Sparse writes beyond the dense capacity, then densifying appends.
|
|
const sparse: number[] = [1, 2, 3];
|
|
sparse[1000] = 4;
|
|
sparse[50] = 5;
|
|
console.log("sparse", sparse.length, sparse[1000], sparse[50], 999 in sparse, sparse.indexOf(5));
|
|
for (let i = 3; i < 60; i++) if (!(i in sparse)) sparse[i] = i;
|
|
console.log("filled", sparse.slice(0, 8).join(","), sparse[49], sparse[50], sparse.lastIndexOf(4));
|
|
sparse.length = 10;
|
|
console.log("truncated", sparse.length, sparse.join(","), 50 in sparse);
|
|
|
|
// Holes and their states across growth and splice.
|
|
const holes: (string | undefined)[] = ["a", , "c", undefined] as (string | undefined)[];
|
|
console.log("holes", holes.length, 1 in holes, 3 in holes, holes.indexOf(undefined));
|
|
holes.push("e", "f", "g", "h", "i");
|
|
holes.splice(1, 2, "B");
|
|
console.log("spliced", holes.length, JSON.stringify(holes), 1 in holes);
|
|
const copy = holes.slice(0, 3);
|
|
console.log("slice", copy.length, copy.join("|"));
|
|
|
|
// Boolean arrays keep one byte per element; their states follow them.
|
|
const flags: boolean[] = [];
|
|
for (let i = 0; i < 40; i++) flags.push(i % 3 === 0);
|
|
flags[45] = true;
|
|
console.log("bools", flags.length, flags.filter((f) => f).length, 42 in flags, flags.indexOf(true, 40));
|
|
flags.length = 5;
|
|
flags.unshift(false);
|
|
console.log("bools after", flags.join(","), flags.length);
|
|
|
|
// Many one-element arrays (the common case the 48-byte header targets).
|
|
const ones: number[][] = [];
|
|
for (let i = 0; i < 1000; i++) ones.push([i]);
|
|
let total = 0;
|
|
for (const one of ones) total += one[0]! + one.length;
|
|
console.log("ones", ones.length, total, ones[999]!.concat(ones[0]!).join(","));
|
|
|
|
// Tagged templates hand out an immortal strings array.
|
|
function tag(strings: TemplateStringsArray, ...values: number[]): string {
|
|
return `${strings.length}:${strings.join("_")}:${values.join(",")}`;
|
|
}
|
|
console.log("tagged", tag`a${1}b${2}c`, tag`only`);
|
|
|
|
// Small arrays filled past their inline slots, of numbers and of class
|
|
// instances that can form cycles.
|
|
class Node {
|
|
readonly id: number;
|
|
readonly next: Node[] = [];
|
|
constructor(id: number) {
|
|
this.id = id;
|
|
}
|
|
}
|
|
const grown: number[][] = [];
|
|
for (let n = 0; n < 10; n++) {
|
|
const a: number[] = [];
|
|
for (let i = 0; i < n; i++) a.push(i * n);
|
|
grown.push(a);
|
|
}
|
|
console.log("grown", grown.map((a) => a.join("-")).join(" "));
|
|
const root = new Node(0);
|
|
for (let i = 1; i <= 9; i++) {
|
|
const child = new Node(i);
|
|
child.next.push(root);
|
|
root.next.push(child);
|
|
console.log("node", i, root.next.length, root.next.map((c) => c.id).join(""), child.next[0]!.id);
|
|
}
|