perf(codegen): collapse the class-field GET tower to one exit - #10189
Closed
proggeramlug wants to merge 2 commits into
Closed
proggeramlug wants to merge 2 commits into
proggeramlug wants to merge 2 commits into
Conversation
added 2 commits
September 13, 2026 13:16
The monomorphic `this.field` / `obj.field` tower in expr/property_get.rs emitted four runtime call sites per site -- `js_typed_feedback_class_field_get_guard` when the PerryTS#5093 inline pre-check missed, `js_throw_type_error_property_access` for a nullish receiver, and one or two `js_object_get_field_by_name_f64` by-name lookups -- spread over class_field_get.{fast,fallback,merge,throw_nullish,fallback_lookup}. In @babel/parser that is 13,636 sites and 27 % of the module's IR, and because every call is a statepoint the .perry_gcmap scaled with it too. Keep the pre-check and the fast slot load byte-identical, and replace the four miss arms with ONE call to `js_class_field_get_ic` -- the runtime helper the PerryTS#5391 path-2 full outline a few lines above already calls. It runs the same guard, reads the same slot at the same header-relative offset on a pass, throws the same nullish TypeError (PerryTS#7153 put that check there for exactly this equivalence) and falls back to `js_object_get_field_by_name_f64`, recording the fallback itself under the same `typed_feedback_enabled()` gate codegen used. A plain number is self-boxing under NaN-boxing, so a `requires_raw_f64` site reads the helper's return exactly as the old phi read the by-name fallback's -- neither arm converted. The key-handle load/bitcast/mask also sinks into the cold miss block, its only remaining consumer. Measured (fixture with one boxed field, two raw-f64 fields, a read on a bare `Named` parameter, reads on `this`, and a raw-f64 read in a loop): * per site: 88 -> 47 IR instructions, 3 runtime calls -> 1, 7 -> 4 blocks; * the `class_field_inline.deref` pre-check is instruction-for-instruction identical (SSA numbering shifts by 3 because the entry block lost the sunk key load); * the `class_field_get.fast` block LOSES its two RS4GC relocation reloads (14 instructions, 2 `call i64 asm "gc-leaf-function"`): it used to be reachable from the guard call, i.e. across a statepoint, and is now reachable only from the pre-check, which crosses none. The inline hit path is strictly shorter than before; * @babel/parser .text 17,640,473 -> 16,389,647 (-7.09 %), .perry_gcmap 809,047 -> 792,749 (-2.01 %), O0-fallback units unchanged. Runtime behaviour is unchanged: on benchmarks/tls-budget/interp.ts the typed-feedback trace reports the same 273 sites with identical guard_passes (817,155,616), guard_failures (202,359,184) and fallback_calls (202,359,184), zero per-site differences, and identical program output. The raw-f64 site's dynamic-fallback native-value record now names `js_class_field_get_ic`; that pair is ADDED to verify/raw_f64.rs rather than replacing the by-name pair, which verify/tests.rs still pins. Tests: a new cargo-test-visible codegen module pins the one-exit shape (the pre-check branches INTO the fast load, the miss arm is exactly one call, the three retired blocks are gone, and the three retired symbols are absent as CALL FORMS -- a bare substring is satisfied by the `declare` line alone). Four runtime tests cover `js_class_field_get_ic`'s arms, which had none. Five existing assertions that used the retired symbols as the proxy for "a guarded class-field read was emitted" are re-pointed: the positives accept either witness (other lowerings still call the old symbols), and the negatives gain the IC as an excluded symbol so they cannot pass on a still-guarded body.
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
proggeramlug
pushed a commit
that referenced
this pull request
Sep 13, 2026
(cherry picked from commit eb4bee5)
Contributor
Author
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Problem
The monomorphic class-field GET tower (
property_get.rs,class_field_get.*) emits per site the #5093 inline precheck plus a guard call, a guard-pass branch, a nullish diamond with its ownjs_throw_type_error_property_accessblock, and one or twojs_object_get_field_by_name_f64fallbacks: ~88 pre-RS4GC IR instructions, 3 runtime call sites, 7 blocks, and 6 RS4GC relocation reloads per site. In @babel/parser that is 13,636 sites, 27 % of the module's IR; every call is a statepoint, so the code and.perry_gcmapscale with call sites × live GC values.Change
Keep the inline precheck and the
class_field_get.fastslot load (boxed and raw-f64 variants, with the raw-f64 bookkeeping) byte-for-byte; replace the guard call / fallback / nullish throw / by-name lookup arms with one call to the existingjs_class_field_get_ic, which runs the same guard, does the fast load on a pass, records the fallback, throws on nullish and falls back to by-name — the contract the full-outline path already used. The key-handle load sinks from the entry block into the cold miss block (its only remaining consumer).class_field_inline_guard.rsis untouched; the runtime crate's non-test code is byte-identical to base.Per site: 88 → 47 IR instructions, 3 → 1 runtime calls, 7 → 4 blocks, 6 → 1 relocation reloads. One unplanned hot-path win:
class_field_get.fastused to be reachable from the guard call, i.e. across a statepoint, so RS4GC put two relocation reloads (14 instructions) on the hit path; it is now reachable only from the precheck and carries none.raw_f64.rsgains("ClassFieldGet", "js_class_field_get_ic")in the dynamic-fallback record set. Tests that used the retired symbols as the witness for "a guarded class-field read was emitted here" now accept either witness; the three negative assertions ("must not rebuild the field IC diamond") gained the new symbol so they cannot pass green on a still-guarded body. New:class_field_get_shape_tests(one exit, boxed and raw-f64, IC operands) and four runtime tests forjs_class_field_get_ic(boxed slot, raw-f64 slot, fallback + by-name, nullish throw).Out of scope, deliberately: the class-field SET tower (T4);
class_field_get.shape_proven_loadand the loop-preheader raw-f64 load (bare loads, nothing to collapse); the number-context tower inproperty_get/helpers.rs(its fallback pipes throughjs_number_coerce, a semantics decision).Evidence (x86-64, perrymaster, base = b5a82cf)
Size,
perry compile --platform bun --no-linkwith the corpus flags:.textbefore.perry_gcmapbefore → afterRuntime A/B, 16 workloads, 5 interleaved rounds, walltime + peak RSS + instructions retired: instructions geomean 0.9997× (largest increase +0.02 %, at the ±0.02 % noise floor of a base-vs-base control); peak RSS between −2.0 % and +0.2 % (floor ±0.6 %); walltime geomean +0.6 % with
interp.ts(class-heavy, ~4 s) at −13.5 %; program outputs identical on every workload (thebench_*programs print their own timings; all checksums match).Cache decisions unchanged:
PERRY_TYPED_FEEDBACK_TRACEoninterp.tsshows 273 sites with identical per-site guard-pass / guard-fail / fallback counters (817,155,616 / 202,359,184 / 202,359,184) in both arms.Gap-suite subset (class, field, this, method, getter, private, static, inherit): 14 pre-existing failures identical in both arms, zero new; one timeout flake reproduced on the base arm too.
Tests
cargo test --release -p perry-codegen: lib 1489 passed / 0 failed / 1 ignored, 25 integration binaries green.RUST_TEST_THREADS=1 cargo test --release -p perry-runtime: 3681 passed, 1 failed —native_stack::tests::stack_top_respects_custom_thread_stack_sizes, which fails identically on a pristine build of base on that Linux host (pre-existing; CI runs the suite on macOS).cargo fmt --all -- --check,scripts/check_file_size.sh,scripts/check_test_registration.py: clean.