From 6013aeb4f821a6aa18c54926552b51698f400f96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 28 Aug 2026 13:02:01 +0200 Subject: [PATCH] =?UTF-8?q?bench:=20populated-object=20delete=20churn=20?= =?UTF-8?q?=E2=80=94=20the=20~200x=20gap=20and=20why=20a=20memo=20cannot?= =?UTF-8?q?=20fix=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit delete obj[k]; obj[k] = v with k rotating over 500 resident keys: node 37 ms, perry ~8000 ms (~200x) — eight times worse than the 0<->1 oscillation, because churn cost scales with resident key count: two 500-element keys clones, two layout rebuilds, two descriptor mints and a 500-slot value shift PER OPERATION. A V8-style delete-transition memo (back-transitions keyed on keys-array address, with the inverse edge recorded at the add-transition funnel) was built, GC-integrated, and passed the full 2762-test suite — then measured as a WASH in drift-cancelling interleaved pairs, twice, and deleted. Rotating keys change the shape every cycle, so address-keyed memoisation never converges; only a same-key churn loop would hit. The structural fix is content-stable shape identity: facts independent of the keys array's address, so equal key-sets reuse one canonical shape regardless of which clone produced them. That is shape interning, not a cache, and it also subsumes the unbounded shape-table growth measured in #8899. This benchmark is the acceptance test for that work. --- benchmarks/bench_populated_delete.ts | 38 ++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 benchmarks/bench_populated_delete.ts diff --git a/benchmarks/bench_populated_delete.ts b/benchmarks/bench_populated_delete.ts new file mode 100644 index 0000000000..16a09a6df2 --- /dev/null +++ b/benchmarks/bench_populated_delete.ts @@ -0,0 +1,38 @@ +// Benchmark: delete/re-add churn on a POPULATED object — the cache/dictionary +// pattern (`delete obj[k]; obj[k] = v` with k rotating over resident keys). +// +// Measured 2026-08-29 (16-core Linux host, node v26.8.1, N = 200_000, +// 500 resident keys): +// +// node 37 ms +// perry ~8_000 ms (~200x) +// +// This is EIGHT TIMES worse than the 0<->1-key delete oscillation +// (`bench_dynamic_property_keys`' delete loop, ~25x after #8936), because the +// churn cost scales with the RESIDENT key count: every iteration pays a +// 500-element keys-array clone on the delete, a second clone on the re-add +// (appending to a shared array), two layout rebuilds, two shape-descriptor +// mints (with `ShapeFacts` hashing and reverse-index maintenance), and a +// 500-slot value shift. O(resident keys) per delete, with large constants. +// +// A delete-transition memo (V8-style back-transitions keyed on the keys +// array's ADDRESS) was built and measured against this benchmark: it was a +// WASH, twice, in drift-cancelling interleaved A/B pairs — with rotating keys +// each delete+re-add changes the shape, so the next delete is a fresh +// (shape, key) pair and address-keyed memoisation never converges. It was +// deleted rather than shipped. The structural fix needs a content-stable +// shape identity (facts independent of the keys array's address), so that +// equal key-sets reuse one canonical shape regardless of which clone produced +// them — that is a shape-interning change, not a cache. +const o: Record = {}; +for (let i = 0; i < 500; i++) o["k" + i] = i; +let s = 0; +const N = 200000; +const t = Date.now(); +for (let i = 0; i < N; i++) { + const k = "k" + (i % 500); + delete o[k]; + o[k] = i; + s += o[k]; +} +console.log("popdel_ms=" + (Date.now() - t) + " chk=" + (s % 7));