mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
for-in over a pure index-signature record (`Record<string, number>`) skips keys deleted during the walk by testing each snapshotted key for presence before visiting it. The test built the overflow map's whole key list and searched it with includes() on every visit, so a walk over n keys took O(n^2) time: 200 walks over 2000 keys took 8.7 s, against 31 ms under Node. The test is now recordOvfHas, the overflow map's hashed presence check, which answers the same question in constant time. The new benchmark workload dictionary-walks walks, prunes and merges Record<string, number> dictionaries with for-in and Object.keys/values/ entries. It takes 7.1 s before this change and 23 ms after (Node 116 ms). Corpus program 7011 pins for-in's visit order and its handling of keys deleted, re-added and added during the walk.
54 lines
1.6 KiB
TypeScript
54 lines
1.6 KiB
TypeScript
// 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}`,
|
|
);
|