-
-
Notifications
You must be signed in to change notification settings - Fork 158
bench(shapes) phase 4: the measurement that refutes dictionary mode, and where the real gap is #8901
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
bench(shapes) phase 4: the measurement that refutes dictionary mode, and where the real gap is #8901
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| // Benchmark: dynamic string-keyed property access, with and without `delete`. | ||
| // | ||
| // Two loops do the SAME number of property writes and reads; only the `delete` | ||
| // differs. Comparing them isolates shape churn from raw property-access cost, | ||
| // which is what makes this benchmark worth keeping: | ||
| // | ||
| // * `deleteHeavy / overwriteOnly` is the *delete penalty* — how much a | ||
| // delete-driven shape walk costs relative to a stable shape. | ||
| // * `overwriteOnly` on its own is *baseline dynamic property throughput*. | ||
| // | ||
| // Measured 2026-08-28 (idle host, perry 0.5.1519, N = 300_000): | ||
| // | ||
| // engine delete_heavy overwrite_only delete penalty | ||
| // node 36 ms 21 ms 1.7x | ||
| // perry 1487 ms 1321 ms 1.1x | ||
| // | ||
| // The delete penalty is the number the "objects that defeat shapes need a | ||
| // dictionary mode" argument rests on — and perry's is LOWER than node's. Adding | ||
| // a dictionary representation would therefore be a large investment aimed at a | ||
| // tail perry does not have. | ||
| // | ||
| // The second column is the real gap: ~60x on plain overwrite. Profiling this | ||
| // binary puts the time in `js_array_get_f64`, `try_read_tracked_gc_header`, | ||
| // `shape_descriptor_by_id` + `shape_descriptor_ensure_with_generation` (two | ||
| // hash lookups per access on the hot path), and `js_put_value_set_dyn_ic_miss` | ||
| // — i.e. inline-cache misses and shape-table probes, not deletion. | ||
| // | ||
| // Keep both columns when changing this file: the ratio is what refutes the | ||
| // dictionary-mode premise, and the absolute is what tracks the real gap. | ||
|
|
||
| function deleteHeavy(n: number): number { | ||
| const o: Record<string, number> = {}; | ||
| let s = 0; | ||
| for (let i = 0; i < n; i++) { | ||
| const k = "k" + (i % 500); | ||
| o[k] = i; | ||
| s += o[k]; | ||
| delete o[k]; // walks the object back to a previous key set | ||
| } | ||
| return s; | ||
| } | ||
|
|
||
| function overwriteOnly(n: number): number { | ||
| const o: Record<string, number> = {}; | ||
| let s = 0; | ||
| for (let i = 0; i < n; i++) { | ||
| const k = "k" + (i % 500); | ||
| o[k] = i; | ||
| s += o[k]; // same writes; the shape stabilises after 500 keys | ||
| } | ||
| return s; | ||
| } | ||
|
Comment on lines
+31
to
+52
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift Align the benchmark and its conclusion. The benchmark compares one-key delete churn with a 500-key stable object. It therefore measures different object sizes and shape paths, not only deletion, and it does not test the stated “thousands of unique keys” workload.
📍 Affects 2 files
🤖 Prompt for AI Agents |
||
|
|
||
| const N = 300000; | ||
|
|
||
| let t = Date.now(); | ||
| const a = deleteHeavy(N); | ||
| const deleteMs = Date.now() - t; | ||
|
|
||
| t = Date.now(); | ||
| const b = overwriteOnly(N); | ||
| const overwriteMs = Date.now() - t; | ||
|
|
||
| console.log( | ||
| "delete_heavy_ms=" + | ||
| deleteMs + | ||
| " overwrite_ms=" + | ||
| overwriteMs + | ||
| " delete_penalty=" + | ||
| (deleteMs / Math.max(overwriteMs, 1)).toFixed(1) + | ||
| "x checksum=" + | ||
| ((a + b) % 7), | ||
| ); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| Added `benchmarks/bench_dynamic_property_keys.ts`, and with it the measurement | ||
| that **refutes the premise for a dictionary mode**. | ||
|
|
||
| The phase-4 plan was a formal dictionary representation for objects that defeat | ||
| shapes — heavy `delete` use, thousands of unique keys — to stop pathological | ||
| objects minting shapes the table must then carry and rekey forever. | ||
|
|
||
| The benchmark runs two loops with the same number of property writes and reads, | ||
| differing only in a `delete`, so the ratio isolates shape churn from raw | ||
| property-access cost. Measured on an idle host, N = 300 000: | ||
|
|
||
| | engine | delete-heavy | overwrite-only | delete penalty | | ||
| |---|---:|---:|---:| | ||
| | node | 36 ms | 21 ms | **1.7×** | | ||
| | perry | 1487 ms | 1321 ms | **1.1×** | | ||
|
|
||
| **Perry's delete penalty is lower than node's.** Delete-driven shape churn is | ||
| not disproportionately expensive here, so a dictionary representation — a new | ||
| object representation, with its own property storage, PIC handling, enumeration | ||
| and GC integration — would be a large investment aimed at a tail perry does not | ||
| have. It is not implemented, and on this evidence should not be until a workload | ||
| shows the penalty that motivates it. | ||
|
|
||
| The second column is the finding worth acting on: **~60× on plain dynamic | ||
| property overwrite**. Profiling that binary attributes it to inline-cache misses | ||
| and shape-table probes, not deletion: | ||
|
|
||
| | samples | symbol | | ||
| |---:|---| | ||
| | 324 | `js_array_get_f64` | | ||
| | 307 | `value::addr_class::try_read_tracked_gc_header` | | ||
| | 105 | `shapes::shape_descriptor_by_id` | | ||
| | 103 | `shapes::shape_descriptor_ensure_with_generation` | | ||
| | 72 | `js_put_value_set_dyn_ic_miss` | | ||
|
|
||
| The two shape-table probes are ~13% of main-thread samples on their own — a | ||
| hash lookup per property access, on a key space (`ShapeId`) that is allocated | ||
| densely by a single atomic counter and could be an array index instead. That is | ||
| a concrete, bounded follow-up; it is not done here because the ids are minted | ||
| from a process-global counter while the tables are per-thread, so a dense `Vec` | ||
| could be sparse on a multi-threaded program, and that trade needs measuring | ||
| rather than assuming. | ||
|
|
||
| Both columns matter when changing this benchmark: the ratio is what refutes the | ||
| dictionary-mode premise, the absolute is what tracks the real gap. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Correct the recorded measurement date.
Line [11] records
2026-08-28, but the current review date is August 27, 2026. Replace it with the completed run date or rerun the benchmark on or after August 28, 2026.🤖 Prompt for AI Agents