-
-
Notifications
You must be signed in to change notification settings - Fork 159
perf(runtime): gate property-tail registry probes by receiver header #7897
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
Merged
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,57 @@ | ||
| ### Performance | ||
|
|
||
| **Property-get IC misses no longer ask the Set and Symbol registries about an | ||
| ordinary object (#7867).** `get_field_by_name_object_tail` already classified | ||
| Buffer and TypedArray before reading the receiver's `GcHeader`, then switched | ||
| on that header for the rest of its exotic-receiver cases. Set and Symbol were | ||
| the exceptions: every miss consulted their address-keyed registries first, | ||
| including the process-global Symbol mutex and hash table. | ||
|
|
||
| Those two exceptions need different gates. Sets are allocated with | ||
| `GC_TYPE_SET`, so the header can rule them out before `SET_REGISTRY` is touched. | ||
| Symbols span two storage classes: fresh `Symbol()` values are GC-backed strings, | ||
| while `Symbol.for`, well-known, and Intl symbols are Box-leaked and have no | ||
| `GcHeader`. Every Symbol does carry `SYMBOL_MAGIC` in its first word, so the | ||
| existing exact-false `may_be_symbol_header` screen rules ordinary receivers out | ||
| without excluding either storage class. In both positive cases the registry | ||
| remains authoritative, including for stale address reuse. | ||
|
|
||
| The ordering remains deliberate. Buffer and TypedArray are headerless and stay | ||
| before the first `GcHeader` read. A possible headerless Symbol returns from the | ||
| magic-gated dispatch before that read. Only then does the tail validate the | ||
| pointer, read the header once, and use `GC_TYPE_SET` to decide whether Set | ||
| dispatch is possible. | ||
|
|
||
| The registered-receiver bodies are `#[cold]` and `#[inline(never)]`. This is not | ||
| cosmetic: an initial version left them inline, grew | ||
| `get_field_by_name_object_tail` by 40 bytes, shifted every following runtime | ||
| function, and regressed `pipeline_big` by **+4.636%** paired geomean (30 | ||
| alternating quiet-M1-mini pairs, 95% CI +4.589% to +4.681%). Sampling showed no | ||
| new hot leaf inside the changed tail; the regression tracked the text-layout | ||
| shift. That version was rejected rather than hidden behind the intended probe | ||
| saving. | ||
|
|
||
| With the cold dispatch outlined, the same locked-host A/B measured 30 | ||
| alternating pairs at 1.693549 s base median and 1.688705 s fixed median: | ||
| **-0.315% paired geomean**, bootstrap 95% CI -0.355% to -0.273%. Both arms | ||
| exited zero and produced the exact `pipeline_big` oracle output | ||
| `556260000 3 3`. | ||
|
|
||
| The issue's earlier 1.8% `is_registered_symbol_slow` attribution is no longer | ||
| present on current `main`: fresh debug-symbol samples contain zero such samples | ||
| in either arm. The structural cost is still directly reproducible. The new | ||
| test arms both registries, performs plain-object misses, and asserts the | ||
| test-only entry counters do not move; before this change the Set counter moves | ||
| from 801 to 803. It then sabotages the Symbol magic screen after a successful | ||
| lookup to prove the Symbol dispatch actually ran and the Set registry remained | ||
| untouched. A companion test exercises Set `.size`, a Box-leaked | ||
| `Symbol.for(...).description`, and a GC-backed `Symbol(...).description`, so | ||
| deleting a positive path or accidentally requiring every receiver to have a | ||
| header turns the suite red. | ||
|
|
||
| Validation includes the full serialized runtime suite (2,154 passed, 4 | ||
| ignored), its doc tests, test-registration and thread-local policy checks, the | ||
| address-class audit, formatting, file-size, and whitespace gates. The | ||
| address-class audit also reports the pre-existing stale `dyn_index.rs` ratchet | ||
| entry (`baseline 2, found 1`); that adjacent cleanup is intentionally not folded | ||
| into this PR. |
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
128 changes: 128 additions & 0 deletions
128
crates/perry-runtime/src/object/field_get_set/get_field_by_name_probe_tests.rs
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,128 @@ | ||
| //! Probe-dispatch invariants for the by-name property-get slow tail (#7867). | ||
| //! | ||
| //! Buffer and small TypedArray allocations can be headerless, so their | ||
| //! registry probes must stay ahead of the first `GcHeader` read. Sets cannot: | ||
| //! every registered Set is `GC_TYPE_SET`. Symbols straddle both storage | ||
| //! classes, but every one carries `SYMBOL_MAGIC` in its own first word. These | ||
| //! tests pin the resulting split instead of merely checking the read result. | ||
|
|
||
| use super::*; | ||
|
|
||
| fn key(bytes: &[u8]) -> *const crate::StringHeader { | ||
| crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32) | ||
| } | ||
|
|
||
| fn leaked_symbol(description: &str) -> usize { | ||
| let description = crate::string::js_string_from_str(description); | ||
| let description = f64::from_bits(crate::value::js_nanbox_string(description as i64).to_bits()); | ||
| let bits = unsafe { crate::symbol::js_symbol_for(description) }.to_bits(); | ||
| let ptr = crate::value::js_nanbox_get_pointer(f64::from_bits(bits)) as usize; | ||
| assert_ne!(ptr, 0, "test premise: Symbol.for allocated a symbol"); | ||
| assert!( | ||
| crate::symbol::is_registered_symbol(ptr), | ||
| "test premise: the leaked symbol is registered on this thread" | ||
| ); | ||
| ptr | ||
| } | ||
|
|
||
| fn fresh_symbol(description: &str) -> usize { | ||
| let description = crate::string::js_string_from_str(description); | ||
| let description = f64::from_bits(crate::value::js_nanbox_string(description as i64).to_bits()); | ||
| let bits = unsafe { crate::symbol::js_symbol_new(description) }.to_bits(); | ||
| let ptr = crate::value::js_nanbox_get_pointer(f64::from_bits(bits)) as usize; | ||
| assert_ne!( | ||
| ptr, 0, | ||
| "test premise: Symbol(description) allocated a symbol" | ||
| ); | ||
| assert!( | ||
| crate::symbol::is_registered_symbol(ptr), | ||
| "test premise: the fresh symbol is registered on this thread" | ||
| ); | ||
| ptr | ||
| } | ||
|
|
||
| fn tail(addr: usize, property: &[u8]) -> JSValue { | ||
| get_field_by_name_object_tail(addr as *const ObjectHeader, key(property)) | ||
| } | ||
|
|
||
| #[test] | ||
| fn plain_object_miss_skips_set_and_symbol_registries() { | ||
| leaked_symbol("perry-7867-arm-symbol"); | ||
| let _set = crate::set::js_set_alloc(4); | ||
| let object = crate::object::js_object_alloc(0, 0) as usize; | ||
|
|
||
| // Warm unrelated lazy state before taking the counters. | ||
| assert!(tail(object, b"missing").is_undefined()); | ||
| let set_before = crate::set::test_set_registry_probe_count(); | ||
| let symbol_before = crate::symbol::test_symbol_registry_probe_count(); | ||
|
|
||
| assert!(tail(object, b"missing").is_undefined()); | ||
| assert_eq!( | ||
| crate::set::test_set_registry_probe_count(), | ||
| set_before, | ||
| "GC_TYPE_OBJECT rules a Set out; a property miss must not probe SET_REGISTRY" | ||
| ); | ||
| assert_eq!( | ||
| crate::symbol::test_symbol_registry_probe_count(), | ||
| symbol_before, | ||
| "a plain-object property miss must not take the process-global symbol mutex" | ||
| ); | ||
|
|
||
| // Sabotage the cheap SYMBOL_MAGIC screen. The authoritative registry probe | ||
| // must return, while the answer and the Set counter stay unchanged. | ||
| let restore = crate::symbol::test_disable_symbol_magic_screen(true); | ||
| let set_before = crate::set::test_set_registry_probe_count(); | ||
| let symbol_before = crate::symbol::test_symbol_registry_probe_count(); | ||
| let answer = tail(object, b"missing"); | ||
| let symbol_after = crate::symbol::test_symbol_registry_probe_count(); | ||
| crate::symbol::test_disable_symbol_magic_screen(restore); | ||
|
|
||
| assert!(answer.is_undefined()); | ||
| assert!( | ||
| symbol_after > symbol_before, | ||
| "with the magic screen defeated the symbol registry probe must return" | ||
| ); | ||
| assert_eq!( | ||
| crate::set::test_set_registry_probe_count(), | ||
| set_before, | ||
| "defeating the symbol screen must not reintroduce the Set probe" | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn set_and_both_symbol_storage_classes_still_dispatch() { | ||
| let set = crate::set::js_set_alloc(4) as usize; | ||
| let set_before = crate::set::test_set_registry_probe_count(); | ||
| let size = tail(set, b"size"); | ||
| assert_eq!(f64::from_bits(size.bits()), 0.0); | ||
| assert!( | ||
| crate::set::test_set_registry_probe_count() > set_before, | ||
| "GC_TYPE_SET must still enter the authoritative Set registry" | ||
| ); | ||
|
|
||
| let symbol = leaked_symbol("perry-7867-headerless-symbol"); | ||
| assert!( | ||
| unsafe { crate::symbol::may_be_symbol_header(symbol as *const u8) }, | ||
| "a Box-leaked symbol must carry SYMBOL_MAGIC without a GcHeader" | ||
| ); | ||
| let symbol_before = crate::symbol::test_symbol_registry_probe_count(); | ||
| let description = tail(symbol, b"description"); | ||
| assert!(description.is_string()); | ||
| assert!( | ||
| crate::symbol::test_symbol_registry_probe_count() > symbol_before, | ||
| "a possible Symbol must still enter the authoritative symbol registry" | ||
| ); | ||
|
|
||
| let symbol = fresh_symbol("perry-7867-gc-symbol"); | ||
| assert!( | ||
| unsafe { crate::symbol::may_be_symbol_header(symbol as *const u8) }, | ||
| "a GC-allocated symbol must carry SYMBOL_MAGIC too" | ||
| ); | ||
| let symbol_before = crate::symbol::test_symbol_registry_probe_count(); | ||
| let description = tail(symbol, b"description"); | ||
| assert!(description.is_string()); | ||
| assert!( | ||
| crate::symbol::test_symbol_registry_probe_count() > symbol_before, | ||
| "a GC-allocated Symbol must still enter the authoritative symbol registry" | ||
| ); | ||
| } |
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
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.