-
-
Notifications
You must be signed in to change notification settings - Fork 161
perf(runtime): stop re-deriving the receiver on every dynamic-key read (−19%) #10570
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
Closed
Closed
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| Stop re-deriving the receiver on every dynamic-key property read. `o[k]` cost | ||
| 674 instructions per access — in a loop, with a constant key, on a | ||
| two-property object — against node's ~4.5, and it did not amortize. A | ||
| symbol-resolved profile showed three per-access re-derivations, the same shape | ||
| as the array-push work: 674.1 -> 546.0, **-19.0%**. | ||
|
|
||
| `keys_find_slot_by_bytes` ran `clean_arr_ptr` on `descriptor.keys` (16.2% of | ||
| the loop) although that field is maintained by the COLLECTOR — | ||
| `shapes::scan_shape_table_rekey_mut` writes the forwarded address back into | ||
| every descriptor record when the keys array moves, and prunes descriptors whose | ||
| keys array died. `try_data_get_bytes` read a thread-local on every access to | ||
| ask whether the receiver is `process.env`; a sticky latch means that is touched | ||
| only once such an object exists. And `is_anon_shape_class_id` took an `RwLock` | ||
| read guard per access (16.1%, the largest frame left) to consult a set written | ||
| only from module init; it is now answered from a lock-free open-addressed | ||
| mirror, sound because the set is insert-only, and living in the class IMAGE | ||
| beside `parent_dense` because `ImageTable` resolves every access through | ||
| `current()`. | ||
|
|
||
| A sticky "is the anon-shape set empty?" latch was tried first and measured | ||
| ZERO — object literals ARE anon shapes, so the flag is true in essentially | ||
| every real program. Caching the resolved slot against the key was dropped: a | ||
| cached key pointer can be recycled into a WRONG slot rather than a miss, and | ||
| would need its own GC root scanner. | ||
|
|
||
| New fixture `test_gap_dynamic_key_read_paths.ts` covers the receiver shapes and | ||
| the mutations that must invalidate what the fast path reads — key added and | ||
| deleted after a first read, 64 keys to cross the indexed-lookup threshold, | ||
| declared-class instances, prototype-chain reads, accessors, and the | ||
| `.constructor`/`getPrototypeOf` verdicts the mirror answers. Byte-identical to | ||
| node; two GC-stress seeds with from-space protection and | ||
| PERRY_GC_FROMSPACE_SCAN_ABORT=1 ran 2,117 and 2,450 copying minors with | ||
| dangling=0 and missing_rewrites=0. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| // Dynamic-key property reads, `o[k]`, across the receiver shapes the fast path | ||
| // in `native_get::try_data_get_bytes` classifies — and the mutations that must | ||
| // invalidate what it reads. | ||
| // | ||
| // Three per-access re-derivations were removed behind this: the keys array was | ||
| // re-resolved through `clean_arr_ptr` although the collector maintains | ||
| // `ShapeDescriptor::keys`; a thread-local was read to answer "is this | ||
| // process.env?"; and an RwLock read guard was taken to ask whether the | ||
| // receiver's class id is an anon shape. The last is answered from a per-IMAGE | ||
| // lock-free mirror, so anything that depends on the anon-shape verdict | ||
| // (`.constructor`, `Object.getPrototypeOf`) is exercised here too. | ||
| const plain: any = { a: 1, b: "two", c: true }; | ||
| for (const k of ["a", "b", "c", "missing"]) console.log("plain", k, String(plain[k])); | ||
|
|
||
| // Anon shape: an object literal's `.constructor` must still be Object, which | ||
| // is the verdict the mirror now answers. | ||
| console.log("ctor is Object:", plain.constructor === Object); | ||
| console.log("proto is Object.prototype:", Object.getPrototypeOf(plain) === Object.prototype); | ||
|
|
||
| // Key added after first read: the shape changes, so the cached keys array must | ||
| // not be reused. | ||
| const grown: any = { x: 1 }; | ||
| console.log("before add", String(grown.y)); | ||
| grown.y = 42; | ||
| console.log("after add", String(grown.y)); | ||
|
|
||
| // Key deleted after first read. | ||
| const shrunk: any = { p: 1, q: 2 }; | ||
| console.log("before delete", String(shrunk.q)); | ||
| delete shrunk.q; | ||
| console.log("after delete", String(shrunk.q)); | ||
|
|
||
| // Many keys, to cross the indexed-lookup threshold the resolved entry | ||
| // deliberately delegates back to the original path for. | ||
| const wide: any = {}; | ||
| for (let i = 0; i < 64; i++) wide["k" + i] = i; | ||
| let sum = 0; | ||
| for (let i = 0; i < 64; i++) sum += wide["k" + i]; | ||
| console.log("wide sum", sum); | ||
|
|
||
| // A declared class instance is NOT an anon shape. | ||
| class Holder { v: number; constructor(v: number) { this.v = v; } } | ||
| const inst: any = new Holder(7); | ||
| console.log("class read", inst["v"], "ctor", inst.constructor === Holder); | ||
|
|
||
| // A prototype-chain read through a dynamic key. | ||
| const parent: any = { inherited: "yes" }; | ||
| const child: any = Object.create(parent); | ||
| child.own = "mine"; | ||
| console.log("own", child["own"], "inherited", child["inherited"]); | ||
|
|
||
| // Accessors must not be answered from the data fast path. | ||
| const acc: any = { get computed() { return "from-getter"; } }; | ||
| console.log("accessor", acc["computed"]); | ||
|
|
||
| // Hot loop over one key, which is the shape the removals were measured on. | ||
| const hot: any = { k: 1.5, other: 2 }; | ||
| let t = 0; | ||
| for (let i = 0; i < 500; i++) t += hot["k"]; | ||
| console.log("hot total", t); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 27738
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 29936
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 14293
Exercise the materialized
process.envpath. This fixture contains noprocess.envexpression, so it cannot materialize the environment object. Its dynamic reads therefore never reach the latch-gatedis_process_env_ptrcheck. A regression in the latch ordering or predicate can leave this fixture passing. Addconst env = process.env; console.log(env["PATH"]);to cover the path. The generic getter declines the fast path for this receiver, then routes the read throughprocess_env_get_field.🤖 Prompt for AI Agents