Test for-in key presence in the overflow map instead of a fresh key list (#1859)

This commit is contained in:
Chris Tate
2026-10-22 16:35:49 -05:00
committed by GitHub
7 changed files with 115 additions and 4 deletions
+1
View File
@@ -11,6 +11,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
| `channel-messages` | cpu | 200,000 MessageChannel messages in one thread, half numbers and half small objects, all posted before the port delivers any |
| `csv-numbers` | cpu | `split`, `Number`, `parseInt`, `toFixed` |
| `cyclic-graphs` | cpu | object graphs with back references built and dropped: trees with parent and sibling links, rings of records sharing a long-lived registry, closures capturing their owner |
| `dictionary-walks` | cpu | `Record<string, number>` dictionaries walked with for-in and `Object.keys`/`values`/`entries` while entries are added, updated and deleted |
| `functional-pipeline` | cpu | `filter`/`map`/`reduce`/`flatMap` chains, closures, small records |
| `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 |
| `json-records` | cpu | `JSON.stringify`, typed `JSON.parse`, Map aggregation |
@@ -0,0 +1,6 @@
{
"entry": "workloads/dictionary-walks.ts",
"args": [
"1"
]
}
@@ -0,0 +1,53 @@
// String-keyed dictionaries (`Record<string, number>`) walked with for-in,
// Object.keys, Object.values and Object.entries while entries are added,
// updated and deleted: per-category totals, a pruning pass and a merge.
let seed = 777;
function random(): number {
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
return seed / 4294967296;
}
const scale = Number(process.argv[2] ?? "1");
const categories = 4000;
const counts: Record<string, number> = {};
for (let i = 0; i < Math.floor(200000 * scale); i++) {
const key = "c" + Math.floor(random() * categories);
counts[key] = (counts[key] ?? 0) + 1;
}
let walked = 0;
let sum = 0;
for (let pass = 0; pass < 40; pass++) {
for (const key in counts) {
walked++;
sum += counts[key]!;
}
}
// Prune: delete the small categories during the walk, including keys the
// walk has not reached yet.
let pruned = 0;
for (const key in counts) {
const n = counts[key]!;
if (n < 45) {
delete counts[key];
pruned++;
}
const ahead = "c" + ((Number(key.slice(1)) + 1) % categories);
if (n > 60 && ahead in counts && counts[ahead]! < 45) {
delete counts[ahead];
pruned++;
}
}
// Merge into a second dictionary and read it back every way.
const merged: Record<string, number> = {};
for (const key of Object.keys(counts)) merged[key.toUpperCase()] = counts[key]! * 2;
let values = 0;
for (const v of Object.values(merged)) values += v;
let entries = 0;
for (const [key, v] of Object.entries(merged)) entries += key.length + v;
console.log(
`walked ${walked}, sum ${sum}, pruned ${pruned}, kept ${Object.keys(counts).length}, values ${values}, entries ${entries}`,
);
@@ -11484,11 +11484,13 @@ function lowerForIn(lowerer: Lowerer, stmt: ts.ForInStatement): IrStmt {
keysT,
loc,
);
// The overflow map's hashed key set answers presence directly; a
// fresh key list per visit would make the walk quadratic.
const guard = (kRef: IrExpr): IrExpr => ({
kind: "arrIntrinsic",
method: "includes",
receiver: { kind: "recordOvfKeys", obj: recvRef(), shapeId, type: keysT, loc },
args: [kRef],
kind: "recordOvfHas",
obj: recvRef(),
shapeId,
key: kRef,
type: BOOL,
loc,
});
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7011-for-in-dictionary-deletes.ts"
],
"diags": []
}
@@ -0,0 +1,42 @@
// for-in over a string-keyed dictionary visits the keys present when the
// walk starts, in insertion order, and skips a key deleted before its
// turn; keys added during the walk are not visited. A key deleted and
// added again before its turn is visited.
const d: Record<string, number> = {};
for (let i = 0; i < 8; i++) d["k" + i] = i;
const seen: string[] = [];
for (const key in d) {
seen.push(key);
if (key === "k1") {
delete d["k3"];
delete d["k0"];
d["k8"] = 8;
}
if (key === "k2") {
delete d["k5"];
d["k5"] = 50;
}
if (key === "k4") delete d["k4"];
}
console.log(seen.join(","));
console.log(Object.keys(d).join(","), JSON.stringify(d));
// A numeric-looking key is walked in integer order before string keys.
const n: Record<string, string> = {};
n["b"] = "x";
n["2"] = "y";
n["10"] = "z";
n["a"] = "w";
const order: string[] = [];
for (const key in n) {
order.push(key);
if (key === "2") delete n["a"];
}
console.log(order.join(","));
// A larger dictionary walked many times.
const big: Record<string, number> = {};
for (let i = 0; i < 3000; i++) big["e" + i] = i;
let total = 0;
for (let pass = 0; pass < 20; pass++) for (const key in big) total += big[key]!;
console.log(total);
@@ -0,0 +1 @@
{ "plain": 1527, "san": 2135 }