-
-
Notifications
You must be signed in to change notification settings - Fork 159
perf(codegen): serve .length on an untyped string receiver inline — pipeline −8.1%, shapes −10.2%
#7905
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
Merged
+297
−15
Merged
perf(codegen): serve .length on an untyped string receiver inline — pipeline −8.1%, shapes −10.2%
#7905
Changes from all commits
Commits
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,47 @@ | ||
| ### Performance | ||
|
|
||
| - **`pipeline` 0.9192×, `shapes` 0.8985×, `asyncpipe` 0.9912× (instructions | ||
| retired) — a `.length` read on a receiver codegen could not prove is a string | ||
| paid the whole object property ladder.** `property_get.rs` already emits a | ||
| three-arm string-`length` dispatch (SSO length byte / heap `utf16_len` load / | ||
| property-semantic slow call) and that dispatch is *fully runtime-guarded* — | ||
| it tests the NaN-box tag and only takes an inline arm for a value that IS a | ||
| string. It was gated on `is_string_expr`, a compile-time proof. A receiver the | ||
| front end cannot type (`rec.tag.length` where `rec` is an object-literal type, | ||
| a JSON `any`, an array element) therefore landed in | ||
| `lower_generic_property_get`, where a heap string can never be served: the | ||
| inline cache requires a `GC_TYPE_OBJECT` receiver by construction (#72). Every | ||
| such read missed to `js_object_get_field_ic_miss` and walked a ladder built | ||
| for objects — a closure-magic deref, buffer and typed-array registry probes, | ||
| then `js_object_get_field_by_name`'s own dispatch, which decoded the key with | ||
| `str::from_utf8` again before reaching the string arm. On | ||
| `gc-handoff/apps/pipeline.ts` that one read was 9.7 % of the program as a | ||
| call-graph subtree. | ||
|
|
||
| The generic tower now splits both string tags out at `.length` sites: a heap | ||
| string (`0x7FFF`) loads `utf16_len` at payload offset 0 through the same | ||
| `safe_load_i32_from_ptr` the proven-string lowering uses, and an SSO receiver | ||
| (`0x7FF9`) extracts the inline length byte instead of calling | ||
| `js_object_get_field_by_name_f64`. Everything else keeps the tower unchanged, | ||
| and the split sits after the typed-feedback observation so a mixed | ||
| object/string site still records every receiver. A non-string receiver pays | ||
| one compare and one branch, and only where the key is `length`. | ||
|
|
||
| Sound by construction: a primitive string's `length` is non-writable, | ||
| non-configurable and cannot be shadowed by an own property, and both string | ||
| tags are disjoint from `POINTER_TAG` — this short-circuits a value the runtime | ||
| ladder computed identically. Same shape as #7753 (array `.length` in the miss | ||
| handler) and #7890 (declared array reads reaching the inline `.length`), one | ||
| receiver type over. | ||
|
|
||
| Validated with two compilers against one pinned runtime pair (the change is | ||
| codegen-only; both `libperry_{runtime,stdlib}.a` compare identical): 19/19 | ||
| corpus programs exit 0 and are byte-identical to `node`, `cmp` across arms | ||
| reads 14 identical / 5 differ and the 5 are exactly the programs that read | ||
| `.length` through the generic tower, the other 14 measure 0.998–1.0004 | ||
| instructions retired, and `iso_miss` still reports `checksum 437840 misses 0` | ||
| under `PERRY_GC_PROTECT_FROMSPACE` and `PERRY_GC_VERIFY_EVACUATION`. | ||
| `test-files/test_gap_dynamic_string_length_generic_tower.ts` feeds the same | ||
| call site a string, an array, array-like objects with numeric and non-numeric | ||
| `length`, a function, a typed array, a number and both nullish values, and | ||
| requires node-identical output including the catchable TypeError. |
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
70 changes: 70 additions & 0 deletions
70
test-files/test_gap_dynamic_string_length_generic_tower.ts
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,70 @@ | ||
| // `.length` read through a receiver the front end cannot type — the generic | ||
| // property-get tower, not the proven-string lowering. | ||
| // | ||
| // The tower now serves a NaN-boxed string (SSO and heap) inline instead of | ||
| // missing the object inline-cache and walking the runtime's object ladder. | ||
| // That short-circuit is only sound if EVERY other receiver still takes the | ||
| // tower unchanged, so this exercises the same call site with a string, an | ||
| // array, array-like objects with numeric and non-numeric `length`, a function, | ||
| // a typed array, a plain number, an object with no `length` at all, and both | ||
| // nullish values (which must throw a catchable TypeError). | ||
|
|
||
| type Box = { payload: any; label: string }; | ||
|
|
||
| function boxed(value: any): Box { | ||
| return { payload: value, label: "box" }; | ||
| } | ||
|
|
||
| // `box.payload` is `any`, so `.length` here lowers through the generic tower. | ||
| function readLength(value: any): any { | ||
| const box = boxed(value); | ||
| return box.payload.length; | ||
| } | ||
|
|
||
| function show(label: string, value: any): void { | ||
| const length = readLength(value); | ||
| console.log(label, String(length), typeof length, length === undefined); | ||
| } | ||
|
|
||
| // SSO string (short) and heap string (long, and a concat result). | ||
| show("sso", "abc"); | ||
| show("empty", ""); | ||
| show("heap", "0123456789012345678901234567890123456789"); | ||
| show("concat", "t:" + "alpha"); | ||
| show("non-ascii", "héllo\u{1F600}"); | ||
|
|
||
| show("array", ["a", "b"]); | ||
| show("array-like number", { length: 7, 0: "z" }); | ||
| show("array-like string", { length: "seven" }); | ||
| show("no length", { other: 1 }); | ||
| show("number", 42); | ||
| show("boolean", true); | ||
| show("typed array", new Uint8Array(3)); | ||
|
|
||
| function twoArgs(a: any, b: any): void { | ||
| void a; | ||
| void b; | ||
| } | ||
| show("function", twoArgs); | ||
|
|
||
| for (const value of [null, undefined]) { | ||
| try { | ||
| readLength(value); | ||
| console.log("nullish", String(value), "no throw"); | ||
| } catch (error) { | ||
| const caught = error as Error; | ||
| console.log( | ||
| "nullish", | ||
| String(value), | ||
| caught.constructor.name + ": " + caught.message, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| // The same site, hot and monomorphic on strings — this is the `pipeline.ts` | ||
| // shape (`rec.tag.length` where `rec` is an object-literal type). | ||
| let total = 0; | ||
| for (let i = 0; i < 200; i++) { | ||
| total = total + readLength("t:" + (i % 3 === 0 ? "alpha" : "be")); | ||
| } | ||
| console.log("total", String(total)); |
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
Scope the helper assertion to the SSO block.
The current search includes the invalid-receiver path. That path always calls
js_object_get_field_by_name_f64. The test can pass if a change removes the required non-lengthSSO helper call.Proposed test fix
let sso = ir .find("\npget.recv_sso") .unwrap_or_else(|| panic!("expected an SSO receiver block:\n{ir}")); + let sso_body = &ir[sso..]; + let sso_end = sso_body[1..] + .find("\n\n") + .map(|i| i + 1) + .unwrap_or(sso_body.len()); assert!( - ir[sso..].contains("js_object_get_field_by_name_f64"), + sso_body[..sso_end].contains("js_object_get_field_by_name_f64"), "a non-`length` SSO read must still call the by-name helper:\n{ir}" );📝 Committable suggestion
🤖 Prompt for AI Agents