Skip to content

interp: admit method_descriptor in the LOAD_ATTR-method fold precondition - #896

Merged
youknowone merged 1 commit into
mainfrom
rewrite-tracer
Jul 30, 2026
Merged

interp: admit method_descriptor in the LOAD_ATTR-method fold precondition#896
youknowone merged 1 commit into
mainfrom
rewrite-tracer

Conversation

@youknowone

@youknowone youknowone commented Jul 30, 2026

Copy link
Copy Markdown
Owner

bound_method_attr_fast_path — the precondition for
try_walker_specialize_load_bound_method_attr — gated on:

if !std::ptr::eq((*w_descr).ob_type, &crate::FUNCTION_TYPE as *const _) { return None; }

The get() it reproduces (baseobjspace.rs:9516-9527) takes the same arm for
FUNCTION_TYPE and METHOD_DESCRIPTOR_TYPE — both with builtin code bind through
w_method_new — and is_function() already covers both. So the predicate was
narrower than the behaviour it mirrors.

Every TypeDef method is retagged method_descriptor
(function_retag_method_descriptor, function.rs:541), so the gate declined
lst.append, d.get, s.add, str.find, lst.pop — every builtin method, i.e.
exactly the shape the fold was written for and names in its own doc. The walker
fell through to the bh_load_attr_fn residual on every iteration:

ForceToken; SetfieldGc ×2; CallMayForceR; GuardNotForced; GuardNoException;
GuardClass; GetfieldGcR Method.w_function; GuardValue; GetfieldGcR Method.w_self

CALL_MAY_FORCE forces the virtualizable frame through GUARD_NOT_FORCED every
iteration, and the Method the fold would have virtualized away is allocated for
real.

BUILTIN_FUNCTION_TYPE stays excluded: get() returns it unbound, so there is no
Method to emit.

Why the PyPy reading misleads here

type(list.append) is method_descriptor in pyre and function in PyPy — so
reading upstream for the shape suggests the FUNCTION_TYPE gate is right. Pyre's
spelling is the correct one for the 3.14 target and PyPy's function is the
PyPy-ism, so the fix is to widen the predicate, not to retype list.append.

How it was localized

MAJIT_STATS for the append loop and the same loop written as a comprehension were
byte-identicalloops_compiled=3 bridges_compiled=3 loops_aborted=3 guard_failures=11742 for both — while exec time differed 8x. That rules out trace
count, aborts and bridge churn in one step. The optimized-trace diff was the 9 ops
above. The residual was identified by matching CallMayForceR(addr, v9, ptr(const), 1) with arg_types: [Ref, Ref, Int] -> Ref against
bh_load_attr_fn(obj, w_code_ptr, name_idx) (call_jit.rs:4871): v9 = the
loop-invariant list, const = the code object, 1 = the co_names index of
"append".

Measurement

A/B at base 8c878513ef with the LLBC corpus re-extracted for it — user+sys CPU
execution time (check.py's own metric), min of 3, dynasm vs pypy:

A (base) B (this commit)
r = []; for i in range(1000): r.append(i) 1.6863s 56.1x 0.2428s 7.8x
synth/const_arg_call_resume (k=12800) 2.4903s 34.0x 1.1180s 16.0x
CallMayForceR in the optimized loop 49 0

6.9x on the target form, 2.2x on the bench. The Method allocation is gone from
the trace as well — the optimizer virtualizes it once the fold emits it as
New+SetField instead of a residual result.

Attribution note: #878 ("inline bound methods and defaults") landed on main while
this was in progress. It is complementary, not overlapping — its
try_walker_inline_builtin_call (inline_call.rs:1994) unwraps a Method
callable into (function, receiver) and inlines the CALL half, and it consumes
the virtual Method this fold emits. Measured on its own it moves the append loop
63.9x → 56.1x and leaves the residual intact; the LOAD_ATTR half is this
commit's.

Verification

At base 8c878513ef, corpus freshly extracted, both backends rebuilt:

  • pyre/check.py --backend dynasm,cranelift342/342 and 342/342, exit 0.
  • cargo test -p pyre-interpreter --features dynasm — 432 passed, 0 failed.
  • cargo test -p pyre-object — 282 passed, 0 failed.
  • A mixed list.append / dict.get / set.add / str.find / list.pop load:
    JIT output identical to PYRE_NO_JIT=1, pypy and cpython.
  • const_arg_call_resume.dynasm.jitstats is unchanged against the committed
    baseline (loops_compiled=3 bridges_compiled=9 loops_aborted=0 guard_failures=1804) — the win is per-iteration cost, not fewer guard failures,
    so no baseline re-record.
  • In the gate the bench moved 22.8x/25.2x → 12.6x/14.0x against its 30x limit.

The branch has since been replayed onto fdca72a7fe, which adds only #891 (already
verified green with and without on the previous base); the numbers above are from
8c878513ef.

Side effect on the perf gate

synth/const_arg_call_resume's max-pypy-ratio=30 was calibrated against a
collapsed denominator: at the committed k=400 pypy's execution time is 0.0061s,
barely above EXEC_TIME_FLOOR_S = 0.005, and the ratio converged upward to
36x/41x as the workload grew — which is why CI reported 41.2x/45.7x while local runs
passed at 22.8x. With this commit k=12800 reads 16.0x, so sizing that bench to an
honest denominator no longer needs the gate moved.

Remaining levers in the same loop (not in this commit)

  • The fold's own w_class and version_tag GuardValues are re-emitted every
    iteration although the receiver is loop-invariant — not hoisted to the preamble
    the way upstream's @elidable_promote version_tag read is.
  • NewWithVtable W_IntObject + SetfieldGc intval followed immediately by
    GetfieldGcI W_IntObject.intval — box then unbox, the allocation surviving even
    though the box is dead after the int-strategy store. All three bench forms pay
    it (7.8x / 15.2x / 20.4x).
  • Three PtrEq/IntIsTrue/Guard chains plus GetfieldGcR PyType.instantiate
    per-iteration strategy/type dispatch after the strategy is already guarded.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Fixed descriptor binding for built-in type methods accessed through get().
    • Improved compatibility between get() and fast-path method binding, ensuring methods such as list.append bind correctly.
  • Tests
    • Added regression coverage for built-in method descriptors and their version tagging.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: fe4bfaac-1918-4007-b77b-4649c4226976

📥 Commits

Reviewing files that changed from the base of the PR and between 229ac4b and 9d5e468.

📒 Files selected for processing (1)
  • pyre/pyre-interpreter/src/baseobjspace.rs

Walkthrough

get() now admits METHOD_DESCRIPTOR_TYPE alongside FUNCTION_TYPE, matching its binding behavior. A regression test verifies that list.append binds through get() and is accepted by bound_method_attr_fast_path.

Changes

Descriptor binding compatibility

Layer / File(s) Summary
Method descriptor admission and regression test
pyre/pyre-interpreter/src/baseobjspace.rs
The descriptor filter accepts method descriptors, and a test verifies list.append binding, descriptor identity, type matching, and version tagging.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: fregataa, kyokuping

Poem

A rabbit checked the method gate,
Where append once met a fussy fate.
“Function or method,” now paths agree,
Bound fast and true as they should be.
Hop, hop—descriptor harmony!

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch rewrite-tracer

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.

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit 9d5e468).
Updated: 2026-07-30T08:41:32.464Z

Files in the reviewed diff
pyre/pyre-interpreter/src/baseobjspace.rs

1. Regressions to PyPy parity introduced by this patch

None.

2. Other mismatches introduced by this patch

None.

3. Pre-existing mismatches (already present before this patch)

None.

4. Structural adaptations

  • pyre/pyre-interpreter/src/baseobjspace.rs:8881 ↔ pypy/objspace/std/callmethod.py:66: Pyre additionally accepts METHOD_DESCRIPTOR_TYPE where PyPy’s direct LOAD_METHOD path accepts only descriptors whose type has flag_method_descriptor. This is a necessary CPython-3.14 descriptor-model/JIT-boundary adaptation: Pyre retags TypeDef builtins such as list.append as method_descriptor, then reproduces the same bound-Method result that PyPy obtains by tracing through space.getattr. The patch corrects a prior overly narrow Pyre predicate; it does not change observable attribute semantics.

…tion

`bound_method_attr_fast_path` gates on `(*w_descr).ob_type == FUNCTION_TYPE`. The
`get()` it reproduces (`baseobjspace.rs:9516-9527`) takes the same arm for
`FUNCTION_TYPE` and `METHOD_DESCRIPTOR_TYPE` — both with builtin code bind
through `w_method_new` — and `is_function` already covers both, so the predicate
was narrower than the behaviour it mirrors.

Every `TypeDef` method is retagged `method_descriptor`
(`function_retag_method_descriptor`), so the gate declined `lst.append`, `d.get`,
`s.add` and every sibling — the shape
`try_walker_specialize_load_bound_method_attr` was written for and names in its
own doc. The walker fell through to the `bh_load_attr_fn` residual on every
iteration: `CALL_MAY_FORCE` + `GUARD_NOT_FORCED` (forcing the virtualizable
frame) + `GUARD_NO_EXCEPTION`, plus the `Method` allocation the fold virtualizes
away. `BUILTIN_FUNCTION_TYPE` stays excluded: `get()` returns it unbound, so
there is no `Method` to emit.

A/B at base 8c87851 with the LLBC corpus re-extracted for it; user+sys CPU
exec (check.py's metric), min of 3, dynasm vs pypy:

                                        A (base)      B (this fix)
    r = []; for i in ...: r.append(i)   1.6863s 56.1x  0.2428s  7.8x
    synth/const_arg_call_resume k=12800 2.4903s 34.0x  1.1180s 16.0x
    CallMayForceR in the optimized loop      49              0

The `Method` allocation is gone from the trace too — the optimizer virtualizes it
once the fold emits it as New+SetField rather than a residual result.

`bound_method_fast_path_admits_the_same_kinds_get_binds` pins the predicate
against `get()` on `list.append`, asserting the premise (a TypeDef method is a
`method_descriptor`) so it fails rather than passing vacuously if the retagging
changes.

Assisted-by: Claude
@youknowone
youknowone merged commit aedb71a into main Jul 30, 2026
7 of 8 checks passed
@youknowone
youknowone deleted the rewrite-tracer branch July 30, 2026 08:39
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