Skip to content

perf: build call frames in place and inline apply_int - #438

Merged
linyiru merged 3 commits into
masterfrom
perf/call-frame
Oct 1, 2026
Merged

linyiru merged 3 commits into
masterfrom
perf/call-frame

Conversation

@linyiru

@linyiru linyiru commented Sep 28, 2026

Copy link
Copy Markdown
Owner

Closes #437. Part of the per-call fixed cost work (#378, #385).

What

  • push_frame_in_place (vm.rs) writes a Frame straight into frames' spare capacity. Five cached method-call push sites in dispatch.rs use it: serve_explicit_recv_method, try_invoke_explicit_recv_block_cached, try_invoke_class_singleton_cached, try_invoke_fixed_method_from_stack, and try_invoke_norecv_block_cached.
  • BinOpKind::apply_int is #[inline(always)].

Why

On starship (x86), while i < n; o.n; i += 1; end retires ~1386 instructions per iteration vs ~328 under CRuby, at a similar IPC. Almost all of the gap is instruction count, which the follow-up ADR will address. Two store-forwarding stalls were the exception, and both are cheap to remove:

  • Frame push. Vec::push takes the 192-byte Frame by value. The frame was assembled on the stack with narrow field stores, then copied with a libc memcpy whose wide loads could not be store-forwarded. That memcpy was ~14% of cycles.
  • apply_int. It was not inlined into BinOpLocalLocal and returned its Option<Value> through memory.

Numbers

starship x86. taskset -c 5 perf stat -e cycles:u, 30M-iteration o.n loop. Master is 69845c0; both binaries were built with line-tables-only.

binary cycles (×1e9) median
master 11.70, 11.88, 12.05, 11.81 (earlier session: 11.72–12.14) ~11.8
this PR 10.70, 10.73, 10.71, 11.87 (earlier session: 10.70–10.82, one 12.17) ~10.7 (−9%)

This PR's results are bimodal: about 1 run in 5 lands at master's level. I have not confirmed why; code or stack alignment is a guess.

On the same machine, the whole shape-bench.rb run took 30.2 → 29.4 e9 cycles (−2.5%, 3 rounds each). Wall-clock call0 on starship stays within noise.

Mac Studio (M2 Max), 5 interleaved rounds, load ~10.

master this PR
call0 o.n 73.0–75.8 ns 70.4–73.3 ns
call0 o.r, o.n1, o.n2, loop — within noise
shape-bench (0-arg / 2-arg / kwargs / **opts / opt+rest) — within noise
Rails hello, median req/s 1011–1065 1008–1101

The Rails numbers are within noise; this change is too small to move Rails measurably. Apple silicon gaining less than x86 is consistent with its cheaper store-forwarding.

Verification (Mac)

  • diff_cruby 1103/1103 in each of four modes: default, STRESS_GC=1, RUBYRS_JIT_NATIVE=1, and tier 2 (THRESHOLD=1)
  • jit unit, lib unit (153), --no-default-features check
  • clippy 1.95 (-D warnings), both the rubyrs everything,jit-native leg and the workspace leg
  • --test embed 421/421
  • perf/check 9/9 within budget
  • lint-gc-rooting and lint-doc-orientation

Not in this PR

  • The block-frame push sites (push_block_frame, invoke_block1/2, …) and the remaining method push sites. They can follow once this lands.
  • The structural per-op and per-call costs: the dispatch loop, the Return path, and the Frame size. An ADR draft will plan these next.

🤖 Generated with Claude Code

Profiling a zero-arg call loop on x86 showed two store-forwarding
stalls on the method-call path. Vec::push takes the 192-byte Frame by
value, so it was assembled on the stack with narrow stores and then
copied with a libc memcpy whose wide loads could not be forwarded;
that memcpy was ~14% of cycles. BinOpKind::apply_int was not inlined
into BinOpLocalLocal and returned its Option<Value> through memory.

push_frame_in_place writes the Frame straight into the vector's spare
capacity at the five cached method-call push sites, and apply_int is
now #[inline(always)]. A 30M-iteration o.n loop drops from ~11.8e9 to
~10.7e9 cycles on starship; Apple silicon gains less (o.n ~73 -> ~71
ns), consistent with its cheaper forwarding.

Closes #437.
@github-actions

github-actions Bot commented Sep 28, 2026 •

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

No unresolved review issues remain.

Review effort: Lite
Findings: None

@linyiru
linyiru merged commit 85e7119 into master Oct 1, 2026
10 of 12 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: remove store-forwarding stalls from the method-call path

2 participants