mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Use an exact integer loop limit as the counted loop's endpoint
A counted for loop converts its limit to an i64 endpoint. A limit that is not a literal went through the double every time: converted to double, clamped to 2^53, rounded with llvm.floor or llvm.ceil and converted back. At the default target CPU the rounding is a libm call, which a loop such as `for (let i = values.length - 1; i > at; i--)` paid on every entry even when `at` was an int32 local. A limit that is an exact integer (an int32 local, an integer view, or a proven range) is its own floor and ceiling and within ±2^53, so it is now the endpoint directly; other limits keep the double path.
This commit is contained in:
@@ -13,7 +13,10 @@ import { integerOrNaNRange } from "./wrapping-sums.js";
|
||||
|
||||
/** The caller has proved or guarded the direction's finite endpoint. Clamp
|
||||
* the opposite infinity before integer conversion. Fractional limits become
|
||||
* exclusive integer endpoints for signed comparisons in either direction. */
|
||||
* exclusive integer endpoints for signed comparisons in either direction. A
|
||||
* limit that is an exact integer (an int32 local, an integer view, or a
|
||||
* proven range) is its own floor and ceiling and within ±2^53, so it is the
|
||||
* endpoint without the double conversion and rounding. */
|
||||
export function emitCountedLoopLimit(
|
||||
host: LlvmEmitterContext,
|
||||
loop: IntegerCountedForLoop,
|
||||
@@ -35,6 +38,14 @@ export function emitCountedLoopLimit(
|
||||
}
|
||||
const B = host.B;
|
||||
const value = host.emitExpr(loop.limit);
|
||||
const exact = exactInteger(host, value, loop.limit);
|
||||
if (exact) {
|
||||
const integer = /^-?\d+$/.test(exact.name) ? exact.name : widenInteger(host, exact);
|
||||
if (!loop.inclusive) return integer;
|
||||
const limit = B.tmp();
|
||||
B.line(`${limit} = add nsw i64 ${integer}, ${ascending ? 1 : -1}`);
|
||||
return limit;
|
||||
}
|
||||
const within = B.tmp(),
|
||||
bounded = B.tmp(),
|
||||
rounded = B.tmp(),
|
||||
|
||||
@@ -438,3 +438,36 @@ test("a loop that starts from a typed-array read guards the read's integer and o
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("an exact integer loop limit becomes the endpoint without rounding", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-counted-limit-"));
|
||||
try {
|
||||
const entry = join(import.meta.dirname, "../../../tests/corpus/counted-loop-integer-limits.ts");
|
||||
const outPath = join(dir, "limits.ir.json");
|
||||
const result = await compile(entry, { outPath, outDir: dir, outputKind: "ir" });
|
||||
if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n"));
|
||||
const ll = emitLlvmModule(deserializeModule(await readFile(outPath, "utf8")), {});
|
||||
const bodies = (method: string): string[] =>
|
||||
[
|
||||
...ll.matchAll(
|
||||
new RegExp(
|
||||
`define internal [^\\n]+ @\\w*Page_${method}(?:\\.i32)?\\([^\\n]*\\) #0 \\{ ; [^\\n]*([\\s\\S]*?)\\n\\}`,
|
||||
"g",
|
||||
),
|
||||
),
|
||||
].map((m) => m[1]!);
|
||||
// The int32 rank is the endpoint of the loops in both directions, with
|
||||
// exclusive and inclusive bounds: no floor, ceil or 2^53 clamp.
|
||||
for (const method of ["set", "suffix"]) {
|
||||
const found = bodies(method);
|
||||
expect(found.length).toBeGreaterThan(0);
|
||||
for (const body of found) {
|
||||
expect(body).toContain("integer induction i");
|
||||
expect(body).not.toMatch(/@llvm\.(floor|ceil)\.f64/);
|
||||
expect(body).not.toContain("0x4340000000000000");
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/counted-loop-integer-limits.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
// Counted for loops whose limit is an exact integer: an int32 local, an
|
||||
// int32 field, the result of a function returning an int32, a value in a
|
||||
// proven range (a masked byte, an array length), counting up and down,
|
||||
// with exclusive and inclusive bounds, empty ranges and the int32 edges.
|
||||
// Fractional, NaN and infinite limits take the double path and give the
|
||||
// same trip counts as Node.
|
||||
|
||||
function bitCount(x: number): number {
|
||||
x = x - ((x >>> 1) & 0x55555555);
|
||||
x = (x & 0x33333333) + ((x >>> 2) & 0x33333333);
|
||||
return Math.imul((x + (x >>> 4)) & 0x0f0f0f0f, 0x01010101) >>> 24;
|
||||
}
|
||||
|
||||
class Window {
|
||||
low = 0;
|
||||
high = 0;
|
||||
constructor(low: number, high: number) {
|
||||
this.low = low | 0;
|
||||
this.high = high | 0;
|
||||
}
|
||||
}
|
||||
|
||||
function countUp(from: number, to: number): number {
|
||||
let trips = 0;
|
||||
for (let i = from; i < to; i++) trips++;
|
||||
return trips;
|
||||
}
|
||||
function countUpInclusive(from: number, to: number): number {
|
||||
let trips = 0;
|
||||
for (let i = from; i <= to; i++) trips++;
|
||||
return trips;
|
||||
}
|
||||
function countDown(from: number, to: number): number {
|
||||
let trips = 0;
|
||||
for (let i = from; i > to; i--) trips++;
|
||||
return trips;
|
||||
}
|
||||
function countDownInclusive(from: number, to: number): number {
|
||||
let trips = 0;
|
||||
for (let i = from; i >= to; i--) trips++;
|
||||
return trips;
|
||||
}
|
||||
|
||||
// An int32 local limit, as in an insertion that shifts the elements after a rank.
|
||||
function insertAt(values: number[], at: number, value: number): void {
|
||||
values.push(value);
|
||||
for (let i = values.length - 1; i > at; i--) values[i] = values[i - 1]!;
|
||||
values[at] = value;
|
||||
}
|
||||
function removeAt(values: number[], at: number): void {
|
||||
for (let i = at; i < values.length - 1; i++) values[i] = values[i + 1]!;
|
||||
values.pop();
|
||||
}
|
||||
|
||||
const values = [10, 20, 30, 40];
|
||||
const rank = bitCount(0b1011 & 0b0011);
|
||||
insertAt(values, rank, 25);
|
||||
insertAt(values, 0, 5);
|
||||
insertAt(values, values.length - 1, 45);
|
||||
console.log("after inserts", values.join(","));
|
||||
removeAt(values, bitCount(1));
|
||||
removeAt(values, values.length - 1);
|
||||
console.log("after removes", values.join(","));
|
||||
|
||||
// A rank from a method as the limit: a page that keeps its values in id
|
||||
// order and shifts the later ones on insert and remove.
|
||||
class Page {
|
||||
readonly values: number[] = [];
|
||||
bits = 0;
|
||||
rank(slot: number): number {
|
||||
return bitCount(this.bits & ((1 << (slot & 31)) - 1));
|
||||
}
|
||||
set(slot: number, value: number): void {
|
||||
const bit = 1 << (slot & 31);
|
||||
const at = this.rank(slot);
|
||||
const values = this.values;
|
||||
if ((this.bits & bit) !== 0) {
|
||||
for (let i = at; i < values.length - 1; i++) values[i] = values[i + 1]!;
|
||||
values.pop();
|
||||
this.bits = (this.bits & ~bit) | 0;
|
||||
return;
|
||||
}
|
||||
values.push(value);
|
||||
for (let i = values.length - 1; i > at; i--) values[i] = values[i - 1]!;
|
||||
values[at] = value;
|
||||
this.bits = (this.bits | bit) | 0;
|
||||
}
|
||||
// Inclusive bounds at a rank, in both directions.
|
||||
suffix(slot: number): number {
|
||||
const at = this.rank(slot);
|
||||
const values = this.values;
|
||||
let total = 0;
|
||||
for (let i = values.length - 1; i >= at; i--) total += values[i]!;
|
||||
for (let i = 0; i <= at - 1; i++) total -= values[i]!;
|
||||
return total;
|
||||
}
|
||||
}
|
||||
const page = new Page();
|
||||
for (const slot of [5, 1, 9, 3, 31, 0, 9, 1, 17]) page.set(slot, slot * 10);
|
||||
console.log("page", page.values.join(","), page.bits >>> 0);
|
||||
console.log("suffix", page.suffix(0), page.suffix(4), page.suffix(17), page.suffix(31));
|
||||
|
||||
// Limits from fields, calls and proven ranges.
|
||||
const window = new Window(-3, 7);
|
||||
let sum = 0;
|
||||
for (let i = window.low; i < window.high; i++) sum += i;
|
||||
console.log("field limits", sum);
|
||||
sum = 0;
|
||||
for (let i = window.high; i >= window.low; i--) sum += i;
|
||||
console.log("field limits down", sum);
|
||||
let byte = 0;
|
||||
for (let k = 0; k < 6; k++) {
|
||||
const limit = (k * 77) & 0xff;
|
||||
let trips = 0;
|
||||
for (let i = 0; i <= limit; i += 37) trips++;
|
||||
byte += trips;
|
||||
}
|
||||
console.log("masked limits", byte);
|
||||
let fromCalls = 0;
|
||||
for (let i = bitCount(0xff); i > bitCount(0x3); i--) fromCalls++;
|
||||
console.log("call limits", fromCalls);
|
||||
|
||||
// Edges and empty ranges.
|
||||
console.log("up", countUp(0, 5), countUp(5, 0), countUp(-5, -2), countUp(2147483645, 2147483647));
|
||||
console.log(
|
||||
"up inclusive",
|
||||
countUpInclusive(0, 5),
|
||||
countUpInclusive(3, 3),
|
||||
countUpInclusive(2147483645, 2147483647),
|
||||
);
|
||||
console.log("down", countDown(5, 0), countDown(0, 5), countDown(-2147483646, -2147483648));
|
||||
console.log(
|
||||
"down inclusive",
|
||||
countDownInclusive(5, 0),
|
||||
countDownInclusive(-2147483646, -2147483648),
|
||||
countDownInclusive(0, 0),
|
||||
);
|
||||
|
||||
// Limits that are not exact integers.
|
||||
console.log("fractional", countUp(0, 2.5), countUpInclusive(0, 2.5), countDown(3, 0.5), countDownInclusive(3, 0.5));
|
||||
console.log("negative fractional", countUp(-3, -0.5), countDownInclusive(0, -2.5));
|
||||
console.log("NaN", countUp(0, Number.NaN), countDown(3, Number.NaN));
|
||||
let capped = 0;
|
||||
for (let i = 0; i < Number.POSITIVE_INFINITY; i++) {
|
||||
if (++capped === 4) break;
|
||||
}
|
||||
console.log("infinite", capped);
|
||||
let floored = 0;
|
||||
for (let i = 3; i > Number.NEGATIVE_INFINITY; i--) {
|
||||
if (++floored === 5) break;
|
||||
}
|
||||
console.log("negative infinite", floored);
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3100, "san": 2000 }
|
||||
Reference in New Issue
Block a user