Skip to content

perf(codegen): inline field precheck for typed-receiver method calls (12.8→3.8 ns) + #9124 review fixes - #9130

Merged
proggeramlug merged 2 commits into
PerryTS:mainfrom
proggeramlug:perf/method-probe-review-followup
Aug 30, 2026
Merged

perf(codegen): inline field precheck for typed-receiver method calls (12.8→3.8 ns) + #9124 review fixes#9130
proggeramlug merged 2 commits into
PerryTS:mainfrom
proggeramlug:perf/method-probe-review-followup

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #9124, which merged with the method-direct probe only (the second commit hadn't been pushed yet). This PR carries the second half of that lever plus the review fixes.

What

  1. One inline field precheck vouches for all typed-receiver fields. With the method probe in front, a probe-first c.inc() loop was ~80% js_typed_feedback_class_field_get_guard: per receiver field, per call, a shape_descriptor_by_id lookup (thread-local map) plus the raw-f64 layout contract, re-deriving facts the exact (class_id, ShapeId) pair already pins. The field-GET sites already emit emit_class_field_inline_precheck ahead of that guard (class/shape + not-forwarded + the per-object raw-f64 intact bit); because the intact bit is object-wide, one precheck vouches for every receiver field at once. Its miss edge runs the unchanged per-field runtime chain, whose i1 result joins at a phi. Same kill switch (PERRY_METHOD_INLINE_PROBE=0), same emission-off gating.
  2. Review fix (perf(codegen): inline probes before the typed-feedback method + field guards (typed-param receivers 118.6→3.8 ns) #9124 / CodeRabbit): emit_inline_direct_method_shape_guard takes accept_raw_ptr. Probe-first sites pass false — a user NaN-box receiver must carry the 0x7FFD tag before any dereference, so a plain double whose bits land in the heap range (a positive subnormal) misses to the runtime guard instead of reaching the header load. The pre-existing shape-only site and versioned_indexed_loop.rs keep raw acceptance (their receivers can legitimately arrive in the internal raw-address ABI). The other review point (receiver lowered before arguments, no re-read) is the tower's pre-existing structure shared by the runtime guard and the shape-only inline guard; all of them reject a moved receiver via the forwarded header bit and fall to the runtime guard, which resolves forwarding — the probe adds no new window, and re-rooting the whole tower is a separate change.
  3. The typed_f64_receiver IR-shape test measured both proofs by the runtime guards' text positions; it now measures the proof itself (inline marker or guard call, whichever dominates), and the fields merge block is created after the precheck's blocks so the typed/generic branch follows the guard calls in emission order.

Measurements

Mac mini, 7 kill-switch pairs, monomorphic c.inc() (node 0.4–0.5 in all four shapes):

receiver / host #9124 as merged (method probe) + field precheck Δ vs node
typed param, plain fn 12.8 3.8 −70% (118.6→3.8 cumulative) 7.6×
captured const, arrow 13.1 3.8 −71% 7.6×
local new C(), plain fn 4.4 4.4 0 8.8×
local new C(), arrow 4.4 4.4 0 11.0×

With both prechecks the loop has zero runtime calls per iteration (sample: 100% inside the generated function). Semantics differential (subclass through base param, virtual override, mid-program prototype monkey-patch, own-property override, annotation-lie receiver) identical to the off arm after all fixes.

Binary size

~80 B per typed-receiver method site (one precheck per site regardless of field count); classhost fixture .text 10,938,772 → 10,939,092 (+320 B, +0.003%) on top of #9124's +384 B. cc-bundle size(1) on/off pair is compiling on perrymaster and will be posted as a comment.

Gates

Validated on the #9124 branch base: -D warnings 0, codegen 1830/0, runtime 2819/0, lints clean, integration 8655 2/2 / 8690 3/3 / 8897 3/3. Rerunning on this base (main with #9103) — results in a comment.

https://claude.ai/code/session_01F1dt1jfzK2cheMZyus6y6p

Summary by CodeRabbit

  • Bug Fixes

    • Improved method dispatch reliability for typed numeric values by validating receiver types and fields more consistently.
    • Added safer handling for supported raw pointer receiver forms during optimized method checks.
    • Strengthened validation sequencing to reduce incorrect execution paths.
  • Tests

    • Updated regression coverage for inline field validation and receiver guard sequencing.

@coderabbitai

coderabbitai Bot commented Aug 30, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5e0c667e-0051-43d3-ae7f-d32bd283362f

📥 Commits

Reviewing files that changed from the base of the PR and between 9e11af0 and c947ba2.

📒 Files selected for processing (1)
  • crates/perry-codegen/src/lower_call/method_override.rs

📝 Walkthrough

Walkthrough

The codegen now controls raw-pointer admission per method-guard call site. Typed-f64 receiver methods use an inline class-field precheck and merge its result with runtime field guards. A regression test updates field-proof ordering.

Changes

Method guard and typed field proof flow

Layer / File(s) Summary
Receiver admission and call-site wiring
crates/perry-codegen/src/lower_call/method_override.rs, crates/perry-codegen/src/stmt/versioned_indexed_loop.rs
emit_inline_direct_method_shape_guard conditionally accepts top-word-zero raw addresses. Inline single-arm guards and versioned indexed loops enable this form. Probe-before-runtime-guard calls disable it.
Typed-f64 field proof merging
crates/perry-codegen/src/lower_call/method_override.rs, crates/perry-codegen/tests/native_proof_regressions.rs
Typed-f64 receiver methods emit an inline class-field precheck when enabled. The proven path merges with per-field runtime guards. The regression test tracks the earliest field proof.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 9e11a

The PR adds guarded inline checks that speed typed-receiver method calls while preserving runtime fallback for rejected values; no actionable merge-blocking risk remains beyond normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant TypedF64Method
  participant ClassFieldPrecheck
  participant RuntimeFieldGuards
  participant TypedCall
  TypedF64Method->>ClassFieldPrecheck: emit class, shape, and raw-f64 field precheck
  ClassFieldPrecheck->>TypedF64Method: return proven path
  TypedF64Method->>RuntimeFieldGuards: evaluate per-field guards when required
  RuntimeFieldGuards->>TypedF64Method: return guard chain
  TypedF64Method->>TypedCall: use merged guard result
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 2 files. (1 skipped: 1… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main optimization: inlining the field precheck for typed-receiver method calls. It also notes the performance result and review fixes.
Description check ✅ Passed The description is detailed and on-topic. It explains the optimization, safety fixes, measurements, binary-size impact, related issue, and validation results. It does not reproduce the template headin…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description is detailed and on-topic. It explains the optimization, safety fixes, measurements, binary-size impact, related issue, and validation results. It does not reproduce the template headings or checklist, but it provides the required information in equivalent sections.

Full details: Docstring Coverage

Explanation

Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 2 files. (1 skipped: 1 too large.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Ralph Küpper added 2 commits August 30, 2026 06:45
…lls + probe review fixes

Follow-up to PerryTS#9124, which merged with the method-direct probe only. This
carries the second half of that lever plus the review fixes:

1. With the method probe in front, the probe-first c.inc() loop was ~80%
   js_typed_feedback_class_field_get_guard: per receiver field, per call,
   it re-derived facts the exact (class_id, ShapeId) pair already pins
   (live slot count, key-at-slot) through a shape_descriptor_by_id lookup
   plus the raw-f64 layout contract. The field-GET sites already emit
   emit_class_field_inline_precheck ahead of that guard (class/shape +
   not-forwarded + the per-OBJECT raw-f64 intact bit). Because the intact
   bit is object-wide, ONE precheck vouches for every receiver field at
   once; its miss edge runs the unchanged per-field runtime chain, whose
   i1 result joins at a phi. Same kill switch (PERRY_METHOD_INLINE_PROBE=0),
   same emission-off gating. Mini pairs: typed-param receiver 12.8 -> 3.8
   ns, captured receiver 13.1 -> 3.8; proven-local rows flat at 4.4 —
   zero runtime calls per iteration (sample: 100% in the generated fn).

2. Review (PerryTS#9124): emit_inline_direct_method_shape_guard takes
   accept_raw_ptr. Probe-first sites pass false — a user NaN-box receiver
   must carry the 0x7FFD tag before any dereference, so a plain double
   whose bits land in the heap range (a positive subnormal) misses to the
   runtime guard instead of reaching the header load. The pre-existing
   shape-only site and versioned_indexed_loop.rs keep raw acceptance.

3. The typed_f64_receiver IR-shape test measured both proofs by the
   runtime guards' text positions; it now measures the proof itself
   (inline marker or guard call, whichever dominates), and the fields
   merge block is created after the precheck's blocks so the typed/generic
   branch follows the guard calls in emission order.

Claude-Session: https://claude.ai/code/session_01F1dt1jfzK2cheMZyus6y6p
@proggeramlug

Copy link
Copy Markdown
Contributor Author

Gates on this base (main @ 07dee0f + #9103): -D warnings 0, codegen 1832/0, lints clean (census, addr-class audit, file-size), integration issue_8655 2/2 / issue_8690 3/3 / issue_8897 3/3. Runtime is untouched by this PR (the full suite ran 2819/0 on the same codegen content earlier). The cc-bundle size(1) on/off pair is still compiling on perrymaster; it lands here as a comment.

@proggeramlug
proggeramlug force-pushed the perf/method-probe-review-followup branch from 9e11af0 to c947ba2 Compare August 30, 2026 04:55
@proggeramlug

Copy link
Copy Markdown
Contributor Author

Merged, plus a rustfmt commit. Follow-up to #9124, which I merged just before this.

Fast-tracked at the maintainer's request, so this got the targeted pass rather than the full treatment — recording exactly what that covered so the gap is visible:

What I did not run this time, and would normally: the 23-shape node differential for method dispatch, the IR A/B confirming the new field precheck actually fires, and a benchmark of the 12.8 → 3.8 ns claim. On #9124 that IR check was worth doing — my fixture there produced byte-identical IR, so the 1.00x I measured meant "never fired", not "no gain". The same caveat applies here: your numbers are unverified by me.

Given this PR also carries "#9124 review fixes", it's worth noting my #9124 review raised one substantive thing that is not addressed here and shouldn't be lost: monkey-patching a prototype is not picked up through a typed-parameter receiver — run(o, n) calling o.m() keeps the old body after P.prototype.m = ..., while a direct p.m() correctly sees the patch. That reproduces identically on main, so it predates both PRs, but it sits squarely in the code this series is changing. I'll file it.

@proggeramlug
proggeramlug merged commit 47bd015 into PerryTS:main Aug 30, 2026
25 of 29 checks passed
@proggeramlug

Copy link
Copy Markdown
Contributor Author

Binary-size row for the inline-probe work (#9124 + #9130), measured on the real bundle.

perrymaster (x86-64), cli_2.1.112.js compiled twice from the same main-built compiler (--no-auto-optimize --enable-wasm-runtime, PERRY_NO_CACHE=1), only PERRY_METHOD_INLINE_PROBE differing:

build .text (size(1)) Δ
probe off 264,432,646
probe on 264,462,586 +29,940 B = +0.0113%

Per-site: the classhost fixture emits 6 method-direct probe blocks + 9 field-proof blocks for +832 B of .text (~139 B/site on x86-64), so the bundle delta corresponds to a few hundred admitted sites out of cli.js's ~17.8k class methods — the admission (typed receiver clone, non-shape-only, feedback emission off) is narrow, as designed.

Method note, since the first attempt at this measurement was vacuous: the earlier pair came out byte-identical because the compiler on that host predated the kill switch, so on == off. This pair is from a GitHub-fetched main (SHA 653e88669) and the switch was verified at the IR level before the compile — method_direct.inline_deref blocks 6 → 0 and fields_proven 9 → 0 between the two settings on a fixture.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant