Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions benchmarks/runtime/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
| `intern-table`, `intern-table-map`, `intern-table-1`, `intern-table-map-1` | cpu | worker threads interning the same type-like objects under string keys and records under number keys, then hits and misses: one shared `InternTable` per kind of key, or a module-level `Map` per thread (the `-map` entries), with 8 threads or 1 (the `-1` entries); under Node.js every thread has its own tables |
| `atomics-contended` | cpu | four worker threads updating `SharedArrayBuffer` counters with `Atomics`: one shared counter, one counter per worker, a `compareExchange` loop, and `Atomics.wait`/`notify` handoffs |
| `value-objects` | cpu | small immutable records and class instances: vector helpers, method chains, particle updates |
| `value-records` | cpu | records that hold strings, spread state updates and destructured tuples: token helpers, an immutable reducer, tuple returns |
| `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 |
| `optional-fields` | cpu | records and class instances with `number \| undefined` and other scalar-union fields |
| `record-boundaries` | cpu | records built from unchecked array reads passed to and returned from annotated functions |
Expand Down
7 changes: 7 additions & 0 deletions benchmarks/runtime/workloads.json
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,13 @@
"2"
]
},
{
"name": "value-records",
"entry": "workloads/value-records.ts",
"args": [
"1"
]
},
{
"name": "region-builds",
"entry": "workloads/region-builds.ts",
Expand Down
93 changes: 93 additions & 0 deletions benchmarks/runtime/workloads/value-records.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Records that hold references: a tokenizer that builds token records with
// string fields and passes them to helpers, an immutable reducer that
// updates its state with object spread, and tuple-returning helpers whose
// results are destructured at once. None of these temporaries outlives
// the statement or loop iteration that builds it.
interface Token {
kind: string;
text: string;
pos: number;
}
const KEYWORDS = ["let", "const", "if", "else", "for", "while", "return", "function"];
function classify(text: string): string {
return KEYWORDS.includes(text) ? "keyword" : "ident";
}
function token(kind: string, text: string, pos: number): Token {
return { kind, text, pos };
}
function weight(t: Token): number {
return t.kind === "keyword" ? t.text.length * 3 : t.text.length + (t.pos & 7);
}
function tokenize(source: string): number {
let sum = 0;
let start = -1;
for (let i = 0; i <= source.length; i++) {
const c = i < source.length ? source.charCodeAt(i) : 32;
const word = (c >= 97 && c <= 122) || (c >= 48 && c <= 57) || c === 95;
if (word) {
if (start < 0) start = i;
continue;
}
if (start >= 0) {
const text = source.slice(start, i);
const t = token(classify(text), text, start);
sum += weight(t);
start = -1;
}
if (c !== 32 && c !== 10) {
const punct = { kind: "punct", text: source[i]!, pos: i };
sum += weight(punct) + (punct.text === ";" ? 1 : 0);
}
}
return sum;
}

interface State {
mode: string;
depth: number;
count: number;
last: string;
}
function step(s: State, text: string): State {
if (text === "{") return { ...s, depth: s.depth + 1, last: text };
if (text === "}") return { ...s, depth: s.depth - 1, last: text };
return s.mode === "code" ? { ...s, count: s.count + 1 } : { ...s, mode: "code", last: text };
}
function reduce(texts: string[], rounds: number): number {
let total = 0;
for (let r = 0; r < rounds; r++) {
let state: State = { mode: "start", depth: 0, count: 0, last: "" };
for (const text of texts) {
state = step(state, text);
if (state.depth > 3) state = { ...state, depth: 3 };
}
total += state.count + state.depth + state.last.length + state.mode.length;
}
return total;
}

function divmod(a: number, b: number): [number, number] {
return [Math.floor(a / b), a % b];
}
function split(text: string, at: number): [string, string] {
return [text.slice(0, at), text.slice(at)];
}
function tuples(n: number, texts: string[]): number {
let sum = 0;
for (let i = 0; i < n; i++) {
const [q, r] = divmod(i * 31, 17);
const [head, tail] = split(texts[i % texts.length]!, r % 3);
sum += q + r + head.length * 2 + tail.length;
}
return sum;
}

const scaleArg = Number(process.argv[2] ?? "1");
const line = "function step ( s , text ) { if ( text ) { return s ; } let x = s ; while ( x ) { x = x ; } }\n";
const source = line.repeat(Math.floor(400 * scaleArg));
const texts = source.split(" ").filter((t) => t.length > 0);
let lexed = 0;
for (let i = 0; i < 240; i++) lexed += tokenize(source);
const reduced = reduce(texts, Math.floor(600 * scaleArg));
const pairs = tuples(Math.floor(2_000_000 * scaleArg), texts);
console.log("tokens", lexed, "reduce", reduced, "tuples", pairs);
6 changes: 6 additions & 0 deletions packages/compiler/src/backend/llvm/call-lifetimes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,12 @@ function collectUses(
)
return true;
break;
case "recordClone":
// A same-shape spread copies the source's fields (retaining the
// references) and keeps no reference to the source itself.
if (node.source.kind !== "varRef") break;
for (const field of node.overrides) expr(field.value);
return true;
case "recordLit":
if (node.type.kind !== "record") break;
for (const field of node.fields)
Expand Down
74 changes: 67 additions & 7 deletions packages/compiler/src/backend/llvm/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ import {
type StringSplit,
} from "./split-loops.js";
import { scalarizeNumericRecords } from "../../ir/scalar-records.js";
import { RESULT_ENTRY_PREFIX, addRecordResultEntries } from "./record-results.js";
import { RESULT_DST_ID, RESULT_ENTRY_PREFIX, addRecordResultEntries } from "./record-results.js";
import { specializeCallbackCalls } from "../../ir/callback-specialization.js";
import { specializeNumericCalls } from "../../ir/numeric-call-specialization.js";
import { everyExpr, everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js";
Expand Down Expand Up @@ -166,7 +166,9 @@ import {
emitStackObject,
findLocalStackObjects,
findReassignedStackObjects,
stackObjectFields,
stackObjectStruct,
type StackObjectField,
type StackObjectSource,
} from "./stack-objects.js";
import {
Expand Down Expand Up @@ -595,6 +597,9 @@ export class LlEmitter {
private reassignedStackObjects = new Set<string>();
/** The binding slot of each reassigned stack-object local. */
private stackObjectSlots = new Map<string, string>();
/** Entry-block pointers to the reference fields of each frame object
* slot (stack-objects.ts stackObjectFields). */
stackObjectFieldPtrs = new Map<string, readonly StackObjectField[]>();
private localUnionStorageProofs = new Map<string, LocalUnionStorageProof>();
private localUnionStorage = new Map<string, LocalUnionStorage>();
integerArrayBindings = new Set<string>();
Expand Down Expand Up @@ -4699,6 +4704,30 @@ export class LlEmitter {
if (isRefCounted(type)) this.currentFrame().push({ name: slot, type, slot: true });
}

/** The innermost lexical scope releases a frame object's reference
* fields when it exits, as it releases its bindings. */
ownScopeFields(fields: readonly StackObjectField[]): void {
const scope = this.scopes[this.scopes.length - 1]!;
for (const field of fields) scope.push({ slot: field.ptr, type: field.type });
}

/** Strike frame-owned slots (ownSlot) from their statement frames: their
* contents are moving to another owner. */
disownSlots(slots: readonly { ptr: string }[]): void {
for (const { ptr } of slots) {
let found = false;
for (let i = this.frames.length - 1; i >= 0 && !found; i--) {
const idx = this.frames[i]!.findIndex((e) => e.slot === true && e.name === ptr);
if (idx >= 0) {
this.frames[i]!.splice(idx, 1);
found = true;
}
}
if (!found)
throw new InternalCompilerError(`llvm emitter bug: disowned slot ${ptr} not in a frame`);
}
}

/** Strike a refcounted temp from its frame: ownership is being moved. */
moveTemp(v: LlValue): void {
if (!isRefCounted(v.type) || this.isImmortal(v.name)) return;
Expand Down Expand Up @@ -6639,6 +6668,7 @@ export class LlEmitter {
? findReassignedStackObjects(this, fn, this.callLifetimes)
: new Set();
this.stackObjectSlots = new Map();
this.stackObjectFieldPtrs = new Map();
this.localUnionStorageProofs = findLocalUnionStorage(fn, this.callLifetimes, this.unionSources);
this.localUnionStorage = new Map();
this.walkBorrows = this.walkBorrowsByFunction.get(fn.name) ?? new Set();
Expand Down Expand Up @@ -7764,12 +7794,18 @@ export class LlEmitter {
}
const stackObject = this.localStackObjects.get(s.localId);
if (stackObject) {
B.line(`store ptr ${emitStackObject(this, stackObject).name}, ptr ${b.slot}`);
const value = emitStackObject(this, stackObject, (fields) => this.ownScopeFields(fields));
B.line(`store ptr ${value.name}, ptr ${b.slot}`);
break;
}
if (s.init && this.reassignedStackObjects.has(s.localId)) {
const slot = this.stackObjectSlot(s.localId, s.init.type);
emitStackObject(this, s.init as StackObjectSource, slot);
emitStackObject(
this,
s.init as StackObjectSource,
(fields) => this.ownScopeFields(fields),
slot,
);
B.line(`store ptr ${slot}, ptr ${b.slot}`);
break;
}
Expand Down Expand Up @@ -7942,12 +7978,28 @@ export class LlEmitter {
break;
}
if (this.reassignedStackObjects.has(s.localId)) {
// The new value may read the old one: build it apart, then copy.
// The new value may read the old one: build it apart (its
// reference fields owned by this statement until it completes),
// then copy it over the binding's slot. The binding takes over the
// new fields; the old ones are released after the slot no longer
// holds them.
const struct = stackObjectStruct(s.value.type);
const value = emitStackObject(this, s.value as StackObjectSource);
let owned: readonly StackObjectField[] = [];
const value = emitStackObject(this, s.value as StackObjectSource, (fields) => {
owned = fields;
for (const field of fields) this.ownSlot(field.ptr, field.type);
});
const target = this.stackObjectSlot(s.localId, s.value.type);
const old = stackObjectFields(this, s.value.type, target).map((field) => {
const t = B.tmp();
B.line(`${t} = load ptr, ptr ${field.ptr}`);
return { name: t, type: field.type };
});
const bits = B.tmp();
B.line(`${bits} = load ${struct}, ptr ${value.name}`);
B.line(`store ${struct} ${bits}, ptr ${this.stackObjectSlot(s.localId, s.value.type)}`);
B.line(`store ${struct} ${bits}, ptr ${target}`);
this.disownSlots(owned);
for (const field of old) this.releaseValue(field.name, field.type);
break;
}
const localStorage = this.localUnionStorage.get(s.localId);
Expand Down Expand Up @@ -8157,7 +8209,15 @@ export class LlEmitter {
s.kind === "fieldSet"
? this.classFieldPtr(obj.name, s.className, s.field)
: this.recordFieldPtr(obj.name, s.shapeId, s.field);
this.replaceField(ptr, type, inline, v);
// A result-slot entry fills zeroed caller storage, each field once.
if (
s.kind === "recordSet" &&
s.obj.kind === "varRef" &&
s.obj.localId === RESULT_DST_ID &&
this.currentFunctionName.startsWith(RESULT_ENTRY_PREFIX)
)
this.initField(ptr, type, inline, v);
else this.replaceField(ptr, type, inline, v);
if (
s.kind === "fieldSet" &&
s.field === "name" &&
Expand Down
5 changes: 4 additions & 1 deletion packages/compiler/src/backend/llvm/expr-calls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,10 @@ export function emitCallExpr(
return emitScalarOptional(host, a);
if (a.kind === "call" && host.scalarResultCall(a))
return emitCallExpr(host, a, false, true);
if (host.stackObjects && canStackObject(host, a)) return emitStackObject(host, a);
if (host.stackObjects && canStackObject(host, a))
return emitStackObject(host, a, (fields) => {
for (const field of fields) host.ownSlot(field.ptr, field.type);
});
if (host.nullableFieldGet(a))
return { name: host.emitOwnedNullableStack(a).box, type: a.type };
// An inline union field read copies its tag and bits.
Expand Down
Loading
Loading