mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-22 05:59:31 +08:00
@scriptc/experimental exports InternTable<K, V>: a named table that every thread of a scriptc program shares and that each thread has its own copy of under Node.js. Keys are strings or numbers with Map equality; values are published (@scriptc/threads) when stored, and the first stored value of a key is kept. Members are get, has, size, getOrInsert and getOrInsertComputed. The runtime table (scr_itable.c) splits each table into 64 segments. Lookups take no lock and do not write; inserts take a segment's spin lock for a probe and a few stores, and callbacks run without a lock. Number tables keep keys and values in the index; string tables keep entries in segment-owned chunks behind tagged slots. Replaced indexes are kept. The compiler lowers the class to a synthetic class with one f64 handle field and five intrinsics, with interned helpers per member and type and an in-place lowering of getOrInsertComputed that creates a function expression callback only on a miss. Stored values go through the publish intrinsic, so their types get the write guard. scriptc refuses names that are not string literals, one name opened with different type arguments, and key or value types a table cannot store. scr_publish walks small graphs in inline buffers and reads SCRIPTC_PUBLISH_TRACE once, instead of allocating and calling getenv on every call.
109 lines
4.1 KiB
TypeScript
109 lines
4.1 KiB
TypeScript
// Concurrent interning from worker threads: every thread interns the same
|
|
// set of type-like objects under string keys (each object refers to two
|
|
// previously interned ones) and records under number keys, then looks keys
|
|
// up (hits and misses). Arguments: the store ("table" for one shared
|
|
// InternTable per kind of key, "map" for a module-level Map per thread, the
|
|
// layout programs use without shared tables), the thread count and the key
|
|
// count. Under Node.js both stores are per-thread Maps.
|
|
import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
|
|
import { InternTable } from "@scriptc/experimental";
|
|
|
|
class Shape {
|
|
readonly id: number;
|
|
readonly name: string;
|
|
readonly left: Shape | undefined;
|
|
readonly right: Shape | undefined;
|
|
constructor(id: number, name: string, left: Shape | undefined, right: Shape | undefined) {
|
|
this.id = id;
|
|
this.name = name;
|
|
this.left = left;
|
|
this.right = right;
|
|
}
|
|
}
|
|
|
|
type Span = { start: number; length: number };
|
|
|
|
const shapeTable = new InternTable<string, Shape>("bench.shapes");
|
|
const spanTable = new InternTable<number, Span>("bench.spans");
|
|
const shapeMap = new Map<string, Shape>();
|
|
const spanMap = new Map<number, Span>();
|
|
|
|
function makeShape(id: number, name: string, useTable: boolean): Shape {
|
|
const left = id > 0 ? shape(Math.floor(id / 2), useTable) : undefined;
|
|
const right = id > 2 ? shape(Math.floor(id / 3), useTable) : undefined;
|
|
return new Shape(id, name, left, right);
|
|
}
|
|
|
|
function shape(id: number, useTable: boolean): Shape {
|
|
const key = `shape:${id}`;
|
|
if (useTable) return shapeTable.getOrInsertComputed(key, () => makeShape(id, key, true));
|
|
let s = shapeMap.get(key);
|
|
if (s === undefined) {
|
|
s = makeShape(id, key, false);
|
|
shapeMap.set(key, s);
|
|
}
|
|
return s;
|
|
}
|
|
|
|
function span(k: number, useTable: boolean): Span {
|
|
if (useTable) return spanTable.getOrInsertComputed(k, () => ({ start: k * 3, length: k % 17 }));
|
|
let s = spanMap.get(k);
|
|
if (s === undefined) {
|
|
s = { start: k * 3, length: k % 17 };
|
|
spanMap.set(k, s);
|
|
}
|
|
return s;
|
|
}
|
|
|
|
function run(useTable: boolean, seed: number, keys: number): string {
|
|
let sum = 0;
|
|
// Interning: each thread walks the keys from its own starting point, so
|
|
// threads insert different keys at first and then find each other's.
|
|
for (let n = 0; n < keys; n++) {
|
|
const id = (n + seed * Math.floor(keys / 8)) % keys;
|
|
const s = shape(id, useTable);
|
|
sum = (sum + s.id + (s.left?.id ?? 0)) % 1000000007;
|
|
sum = (sum + span(id * 7, useTable).length) % 1000000007;
|
|
}
|
|
// Lookups: three hits and one miss per key, of each kind.
|
|
let hits = 0;
|
|
for (let round = 0; round < 3; round++)
|
|
for (let n = 0; n < keys; n++) {
|
|
const id = (n * 7919 + round) % keys;
|
|
const key = `shape:${id}`;
|
|
const s = useTable ? shapeTable.get(key) : shapeMap.get(key);
|
|
if (s !== undefined && s.name === key) hits++;
|
|
const p = useTable ? spanTable.get(id * 7) : spanMap.get(id * 7);
|
|
if (p !== undefined && p.start === id * 21) hits++;
|
|
}
|
|
let misses = 0;
|
|
for (let n = 0; n < keys; n++) {
|
|
const absent = useTable ? shapeTable.has(`shape:-${n + 1}`) : shapeMap.has(`shape:-${n + 1}`);
|
|
if (!absent) misses++;
|
|
if (!(useTable ? spanTable.has(n * 7 + 1) : spanMap.has(n * 7 + 1))) misses++;
|
|
}
|
|
return `sum=${sum} hits=${hits} misses=${misses}`;
|
|
}
|
|
|
|
if (isMainThread) {
|
|
const store = process.argv[2] ?? "table";
|
|
const threads = Number(process.argv[3] ?? "8");
|
|
const keys = Number(process.argv[4] ?? "100000");
|
|
const results: string[] = [];
|
|
let pending = threads;
|
|
for (let t = 0; t < threads; t++) {
|
|
const worker = new Worker(new URL(import.meta.url), {
|
|
workerData: { useTable: store === "table", seed: t, keys },
|
|
});
|
|
worker.on("message", (message: unknown) => {
|
|
results[t] = message as string;
|
|
});
|
|
worker.on("exit", () => {
|
|
if (--pending === 0) for (let i = 0; i < threads; i++) console.log(`thread ${i}: ${results[i]}`);
|
|
});
|
|
}
|
|
} else {
|
|
const data = workerData as { useTable: boolean; seed: number; keys: number };
|
|
parentPort!.postMessage(run(data.useTable, data.seed, data.keys));
|
|
}
|