-
-
Notifications
You must be signed in to change notification settings - Fork 158
perf(gc): stop the globalThis bootstrap from disabling the per-object layout fast path #7809
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
proggeramlug
merged 1 commit into
main
from
perf/7796-globalthis-bootstrap-layout-latch
Aug 11, 2026
Merged
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,100 @@ | ||
| ### Fixed | ||
|
|
||
| - **Touching `globalThis` no longer disables the per-object GC layout fast path | ||
| for the rest of the process.** `churn` and `tree` were paying +28% / +29% for | ||
| a single `for…of` in `main()`, and every real TypeScript program was paying it | ||
| too. | ||
|
|
||
| `globalThis` is populated lazily on first touch, and *any* plain-object or | ||
| array property **miss** forces it: the miss walks the prototype chain, reaches | ||
| `builtin_prototype_value` → `js_get_global_this_builtin_value`, and runs the | ||
| several-hundred-builtin bootstrap. Perry's whole benchmark corpus happens | ||
| never to take that path — no `for…of`, no spread, no `Symbol`, no property | ||
| miss anywhere in `churn`/`tree`/`interp`/`shapes`/`asyncpipe`/`retain` — so | ||
| the cost was invisible to every number in the perf campaign while real | ||
| programs paid it before their first line of work. | ||
|
|
||
| It was **not** the ~1.15 MB the bootstrap allocates. GC behaviour is | ||
| effectively identical with and without it: 105 minors on `churn` either way, | ||
| and ~616 KB more copied across the entire run. The cost was a **global latch**. | ||
| The bootstrap builds hundreds of permanently-rooted plain objects, and each | ||
| one's first pointer field minted an entry in the per-object GC slot-layout | ||
| side tables. Those entries are immortal, so `PER_OBJECT_LAYOUTS_NONEMPTY` — | ||
| the emptiness proof that keeps `layout_forget_object` off the allocation, | ||
| death and relocation paths — could never go `false` again. Measured | ||
| `layout_forget_object` self time: `churn` 112 ms → 916 ms, `tree` | ||
| 194 ms → 740 ms, which is essentially the whole regression in both. | ||
|
|
||
| This is #7510's lesson at 1000× the scale ("one immortal entry is enough to | ||
| nullify an is-empty accelerator"; there it was a single interned keys array). | ||
| Two changes, and **both are needed**: | ||
|
|
||
| 1. `gc::ImmortalLayoutScope` around `populate_global_this_builtins`. Inside | ||
| it, an object that would mint a per-object pointer mask declares | ||
| `GC_LAYOUT_UNKNOWN` instead — the tag-checked payload scan, which is the | ||
| code's own fallback for the same situation and the universally safe state, | ||
| not a weaker one. For an object that is never reclaimed the mask bought | ||
| precision nobody spends, at the price of two `RefCell` round-trips and two | ||
| hash probes on every allocation the program would ever make. Bootstrap | ||
| residue: **1113 entries → 0**. | ||
|
|
||
| Deliberately **not** applied to typed-shape layouts | ||
| (`init_typed_shape_layout`): those describe raw-f64 slots, whose bit | ||
| patterns can alias a heap pointer, and a conservative scan would trace — | ||
| and under the copying collector *rewrite* — a slot holding a number. The | ||
| scope applies only where the mask being replaced is itself derived from | ||
| `layout_pointer_bearing_bits`, i.e. exactly the test `GC_LAYOUT_UNKNOWN` | ||
| re-runs per slot. | ||
|
|
||
| 2. An **address filter** replacing `PER_OBJECT_LAYOUTS_NONEMPTY` as the hot | ||
| guard. Change 1 alone moved nothing measurable, and that is the important | ||
| finding: ordinary runtime init still leaves one or two long-lived records | ||
| behind, and for a single global bit two entries are exactly as bad as 1113. | ||
| An 8192-bit thread-local filter over the key addresses turns "is either | ||
| table empty?" into "can this *address* have an entry?", so a nursery | ||
| address the tables have never seen is proved absent in one multiply and one | ||
| load even while immortal records exist elsewhere. | ||
|
|
||
| The filter sits *behind* the flag, and both live in ONE thread-local | ||
| (`PerObjectLayoutHint`). All three arrangements were measured on the quiet | ||
| mini; the co-located one wins everywhere: | ||
|
|
||
| | | `churn` | `push_cls` | `tree` | `interp` | `churn`+`for…of` | `tree`+`for…of` | | ||
| |---|--:|--:|--:|--:|--:|--:| | ||
| | base | 0.422 | 0.356 | 1.627 | 1.888 | 0.539 | 2.151 | | ||
| | filter only (no flag) | 0.438 | **0.383** | 1.673 | 1.934 | 0.500 | 1.857 | | ||
| | flag + filter, 2 slots | 0.421 | 0.368 | 1.642 | 1.950 | 0.506 | 1.886 | | ||
| | **flag + filter, 1 slot** | **0.422** | **0.368** | **1.640** | **1.922** | **0.493** | **1.840** | | ||
|
|
||
| Dropping the flag is a loss: almost every workload is *disarmed*, and for | ||
| those the flag is one load where the filter is a multiply, a shift, a load | ||
| and a test — `push_cls` went past its budget. Keeping both as separate | ||
| thread-locals costs a second `_tlv_get_addr` on exactly the workloads that | ||
| are legitimately armed (`interp`, `iso_miss`). One struct behind the | ||
| existing named hot slot gives the cheap gate AND one resolution. | ||
|
|
||
| `false` is a proof of absence and nothing else rests on it; the filter is | ||
| rebuilt from the live keys once half its bits are set, so a workload that | ||
| genuinely churns per-object records cannot saturate it permanently. | ||
|
|
||
| Measured on the quiet mini (base = `b9415d780`, both arms built locally; | ||
| interleaved, best-of-5, exit-checked): `churn` + `for…of` **0.539 → 0.493** | ||
| (floor 0.422), `tree` + `for…of` **2.151 → 1.840** (floor 1.627). Every | ||
| protected bench stays inside budget. `interp` 1.888 → 1.922 and `iso_miss` | ||
| 2.361 → 2.443 still pay the filter test without benefiting from it — they are | ||
| legitimately armed, so it never proves absence for them. | ||
|
|
||
| Gated by five tests in `gc::tests::layout_trace::per_object_tables`, written | ||
| so none of them can pass vacuously: the bootstrap must leave both tables empty | ||
| (with a subject-live check that `globalThis.Array` actually populated); the | ||
| same store *outside* a scope must still mint a mask; an object built inside a | ||
| scope must still trace its children through the fallback scan; a live record | ||
| must survive a filter rebuild and still be found and removed; and — the | ||
| accelerator's own subject-live assertion — the filter must still prove | ||
| unrelated addresses absent *while the global flag is armed*, which is the | ||
| exact condition under which it silently stopped accelerating before. | ||
|
|
||
| `PERRY_GC_DIAG=1` now prints | ||
| `[gc-globalthis-bootstrap] elapsed_us=… per_object_slot_masks=… per_object_typed_layouts=…` | ||
| once per thread, so the bootstrap's cost and its residue are observable rather | ||
| than inferred. | ||
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
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.
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
Stale 8192-bit filter size in prose.
crates/perry-runtime/src/gc/layout_tables.rsLine 92 setsLAYOUT_ADDR_FILTER_BITSto 4096, and its doc comment describes 4096 bits / 512 B. Two prose sites still describe an 8192-bit filter, so the filter size was reduced without updating the text.changelog.d/7809-globalthis-bootstrap-layout-latch.md#L49-L56: change "An 8192-bit thread-local filter" to "A 4096-bit thread-local filter". Release notes are assembled from this fragment, so the published number would be wrong.crates/perry-runtime/src/gc/tests/layout_trace/per_object_tables.rs#L439-L440: change "one live record in 8192 bits" to "one live record in 4096 bits". The assertion threshold on Line 455 still holds at 4096 bits, so only the comment needs the edit.📍 Affects 2 files
changelog.d/7809-globalthis-bootstrap-layout-latch.md#L49-L56(this comment)crates/perry-runtime/src/gc/tests/layout_trace/per_object_tables.rs#L439-L440🤖 Prompt for AI Agents