Skip to content

perf: plain-return fast path for Op::Return (ADR 0039 I1) - #440

Merged
linyiru merged 2 commits into
masterfrom
perf/plain-return
Sep 29, 2026
Merged

linyiru merged 2 commits into
masterfrom
perf/plain-return

Conversation

@linyiru

@linyiru linyiru commented Sep 28, 2026

Copy link
Copy Markdown
Owner

Closes #439. This is increment I1 of the new ADR 0039 (per-call and per-op fixed cost), which this PR also adds. Part of #378 / #385. It is independent of #438.

ADR 0039

  • Trace. On starship, a gdb stepi trace of one while i < n; o.n; i += 1; end iteration counts 1385 instructions. CRuby runs the same iteration in ~328, at a similar IPC.

  • Where they go.

    part instructions
    call op ~550
    Op::Return ~260
    per-op overhead: the dispatch loop plus check_fuel (~50 per op, 8 ops) ~400
  • Scope. ADR 0033 already ruled out a wholesale lean rewrite and hot-op inlining. This ADR goes after something narrower: the bookkeeping every op and every call/return pays even when the feature it serves is not in use.

  • Plan. Five increments, each as its own PR, each gated on a measured cycles gain:

    1. I1: plain-return fast path (this PR)
    2. I2: code-slice cache
    3. I3: one safe-point countdown in place of the per-op fuel and interrupt checks
    4. I4: a smaller Frame and a merged call chain
    5. I5: fused compare-and-branch

I1: what changed

Op::Return now tests one conjunction first. It holds when:

  • no method break or loop transfer is pending,
  • the frame is not the kept iterator block,
  • the frame has no aux box (so no ensure, rescue, loop or $! state),
  • the frame is not a class body, and has no swap_return and no dm_share.

When it holds, Return:

  1. takes locals and saved_last_match out of the frame,
  2. drops the frame in place (truncate, rather than moving 192 bytes out with pop),
  3. restores $~,
  4. rebalances the operand stack only if the body left residue,
  5. releases the locals.

Every other shape falls through to the unchanged slow path.

The draft ADR planned a Frame::plain_return flag computed at push. It turned out not to be needed: the tested fields share the frame's cache lines. Testing them directly also means there is no flag invariant to maintain at every site that makes a frame non-plain.

Numbers

starship (x86). taskset -c 5 perf stat, 5 interleaved rounds. Base is master 701b0de; both builds use line-tables-only.

master this PR
o.n loop (30M iterations), instructions 41.65e9 38.74e9 (−97 per iteration)
o.n loop, cycles 11.72–12.16e9 10.18–10.34e9 (−13%)
shape-bench.rb cycles, 3 rounds 30.06 / 30.16 / 30.42e9 29.12 / 29.19 / 30.57e9
call0 o.n 81.8 ns 70.5 ns
call0 o.n1(i) 87.4 ns 76.5 ns
call0 o.n2(i,i) 91.8 ns 81.9 ns

Mac Studio (M2 Max). 5 interleaved rounds for call0 and Rails, 3 for shape-bench (medians). The load average was ~8–13.

master this PR
call0 o.n 72.2–77.5 ns 69.8–72.1 ns
call0 o.n1 81.8–84.1 ns 76.1–80.2 ns
call0 o.n2 87.5–90.0 ns 81.5–86.0 ns
shape 0-arg 64.6 59.5 (−8%)
shape 2-arg 75.9 71.3 (−6%)
shape kwargs 83.3 81.5
shape opt+rest 125.2 121.7
shape **opts 338 339
Rails hello, median req/s 1039–1079 (~1054) 1063–1093 (~1078, +2%)

The Rails runs were under load, so read the Rails number as a direction, not a measurement.

Verification (Mac)

  • diff_cruby 1103/1103 in each of four modes: default, STRESS_GC=1, RUBYRS_JIT_NATIVE=1, and tier 2 with THRESHOLD=1. The new plain_return.rb also passes under default and STRESS_GC=1.
  • jit unit, lib unit (153), --no-default-features check (this covers the regex-off cfg).
  • clippy 1.95 with -D warnings, both the rubyrs and the workspace leg.
  • --test embed 421/421, perf/check 9/9, lint-gc-rooting, lint-doc-orientation.

New fixture plain_return.rb covers:

  • early and mid-expression return
  • $~ being method-local across a callee
  • return through an ensure and out of a while (both take the slow path)
  • block return values, recursion, a 10k-call loop
  • a class body followed by a call

Found in passing, not fixed here

r = class Foo; 7; end gives r == Foo in rubyrs and 7 in CRuby. This is already true on master. The code comment in Op::Return calls it rubyrs's return-the-class behaviour, but it is not documented in docs/SUBSET.md.

🤖 Generated with Claude Code

An instruction trace of one call-loop iteration on x86 (1385 vs
CRuby's ~328 at similar IPC) shows the gap is spread over the call
path, Op::Return, and ~50 instructions of per-op dispatch/fuel
overhead. ADR 0033 ruled out a wholesale lean rewrite, so this plans
five narrow increments that each remove bookkeeping paid when the
feature it serves is unused, each gated on a measured cycles gain.
A plain method return paid ~260 instructions on x86 for checks that
find nothing to do: the ensure scan, pending break and kept-block
tests, a 192-byte frames.pop() move, the transfer sweep, and the
$!/class-body/swap_return/aux tests. When no transfer is in flight
and the frame has no aux box, class body, swap_return or dm share,
Return now drops the frame in place, restores $~, rebalances the
stack only if needed, and releases the locals. Every other shape
takes the unchanged slow path.

The o.n call loop retires 97 fewer instructions per iteration and
takes 13% fewer cycles on starship (ADR 0039 I1).

Closes #439.
Copilot AI lite review requested due to automatic review settings September 28, 2026 21:23
@github-actions

Copy link
Copy Markdown

gapscan PR diff

This PR doesn't touch any file that affects gapscan classification — SUPPORTED_PRISM_NODES / RIDES_ALONG_PRISM_NODES are unchanged. Skipped the base build and the 10 per-target scans.

See docs/gap-reports/ for the dataset and methodology.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The only noted issue is a non-blocking documentation nit.

Review effort: Lite
Findings: None

@linyiru
linyiru merged commit 9c4f587 into master Sep 29, 2026
13 of 14 checks passed
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.

perf: plain-return fast path for Op::Return

2 participants