Reuse a string-key Map read's entry for the following write

`const cur = m.get(k); m.set(k, f(cur))` and `m.set(k, (m.get(k) ?? 0) + 1)`
probed a string-keyed map's hash index twice. When only inert code (reads,
scalar arithmetic, compiler-resolved throws) runs between the read and the
write, the read parks the map, the key and the resolved entry in local
slots and the write overwrites that entry's value in place if the map and
key are identical at run time and the entry was found. Misses and every
other case take the ordinary set, so insertion order and SameValueZero
semantics are unchanged. Only string-keyed typed maps with scalar values
fuse; number-key writes after a read already store into the found entry
through findReadNumberMapWrites.
This commit is contained in:
Malte Ubl
2026-10-16 06:54:44 -07:00
parent fc89fec9b9
commit bf5c8e7b2c
8 changed files with 691 additions and 9 deletions
+135
View File
@@ -0,0 +1,135 @@
// Map get followed by set on the same key reuses the read's entry when
// nothing in between can touch a collection. Present and absent keys, NaN
// and signed zeros, aliases, other maps and keys, and intervening mutations
// must keep Node's values and insertion order.
function show(label: string, m: Map<string, number>): void {
const parts: string[] = [];
for (const [k, v] of m) parts.push(k + "=" + v);
console.log(label, m.size, parts.join(" "));
}
function showNumbers(label: string, m: Map<number, number>): void {
const parts: string[] = [];
for (const [k, v] of m) parts.push((Object.is(k, -0) ? "-0" : String(k)) + "=" + v);
console.log(label, m.size, parts.join(" "));
}
// Counting: the first occurrence inserts, later ones overwrite in place.
const words = "the cat and the hat and the bat".split(" ");
const counts: Map<string, number> = new Map();
for (const w of words) {
const cur = counts.get(w);
counts.set(w, (cur === undefined ? 0 : cur) + 1);
}
show("counts", counts);
// The single-statement form.
const lengths: Map<string, number> = new Map();
for (const w of words) lengths.set(w, (lengths.get(w) ?? 0) + w.length);
show("lengths", lengths);
// Number keys: NaN finds NaN, -0 and +0 are one key (SameValueZero).
const nums: Map<number, number> = new Map();
const keys = [1, NaN, -0, 0, 2, NaN, 0, -0, 1, Number.NaN];
for (const k of keys) {
const prev = nums.get(k);
nums.set(k, prev === undefined ? 100 : prev + 1);
}
showNumbers("nums", nums);
console.log("has -0", nums.has(-0), "get 0", nums.get(0), "get NaN", nums.get(NaN));
// A NaN value is stored and read back.
const nanValues: Map<string, number> = new Map();
nanValues.set("x", 1);
const x0 = nanValues.get("x");
nanValues.set("x", (x0 ?? 0) * NaN);
console.log("nan value", nanValues.get("x"));
// An alias of the same map, and a write to a different map or key.
const a: Map<string, number> = new Map([
["p", 1],
["q", 2],
]);
const alias = a;
const other: Map<string, number> = new Map([["p", 50]]);
const p1 = a.get("p");
alias.set("p", (p1 ?? 0) + 10);
const p2 = a.get("p");
other.set("p", (p2 ?? 0) + 1);
const q1 = a.get("q");
a.set("r", (q1 ?? 0) + 3);
show("a", a);
show("other", other);
// The map changes between the read and the write: delete re-appends,
// clear empties, a fresh insert grows the table.
const m: Map<string, number> = new Map([
["k1", 1],
["k2", 2],
["k3", 3],
]);
const v1 = m.get("k1");
m.delete("k1");
m.set("k1", (v1 ?? 0) + 1);
show("deleted", m);
const v2 = m.get("k2");
m.set("k2", (v2 ?? 0) + (m.delete("k3") ? 100 : 0));
show("delete inside", m);
const v3 = m.get("k2");
m.clear();
m.set("k2", (v3 ?? 0) + 1);
show("cleared", m);
for (let i = 0; i < 40; i++) {
const key = "g" + (i % 17);
const before = m.get(key);
m.set(key, (before ?? 0) + i);
}
show("grown", m);
// Keys built fresh for the write still update the same entry.
const fresh: Map<string, number> = new Map();
for (let i = 0; i < 6; i++) {
const got = fresh.get("id" + (i % 2));
fresh.set("id" + (i % 2), (got ?? 0) + i);
}
show("fresh", fresh);
// Boolean values toggle in place.
const flags: Map<number, boolean> = new Map();
for (let i = 0; i < 7; i++) {
const k = i % 3;
const f = flags.get(k);
flags.set(k, f === undefined ? true : !f);
}
console.log("flags", [...flags.entries()].map(([k, v]) => k + ":" + v).join(" "));
// Inside a function, behind a condition, and with a throwing key read.
type Row = { cat: string; v: number };
function aggregate(rows: Row[]): Map<string, number> {
const agg: Map<string, number> = new Map();
for (const r of rows) {
if (r.v < 0) continue;
const cur = agg.get(r.cat);
agg.set(r.cat, (cur === undefined ? 0 : cur) + r.v);
}
return agg;
}
const rows: Row[] = [
{ cat: "b", v: 2 },
{ cat: "a", v: 1 },
{ cat: "b", v: -5 },
{ cat: "c", v: 4 },
{ cat: "a", v: 8 },
];
show("aggregate", aggregate(rows));
try {
const maybe: (Row | undefined)[] = [rows[0], undefined];
const agg: Map<string, number> = new Map();
for (const r of maybe) {
const cur = agg.get((r as Row).cat);
agg.set((r as Row).cat, (cur ?? 0) + 1);
}
} catch (e) {
console.log("threw", e instanceof TypeError);
}