perf(codegen): inline small hot (in-loop) functions via inlinehint#6855
Conversation
After native Math.imul + typed-array-param reads (#6850), the only residual gap of a tight integer-math kernel vs V8 is function-call overhead: V8 inlines a small hot function into its loop; Perry left it out-of-line. A NaN-boxed bit-mixer (`mix`, ~10 statements) costs ~800 in LLVM's inline model once GC shadow-frame calls + typed-array reads + double<->i32 marshaling are counted — well above the base -O3 threshold — so -O3 kept it as a call. Bias, don't force. Perry keeps `alwaysinline` only for the genuinely tiny (<= 8 statements). This adds a distinct `inlinehint` path for functions that are: - small (9..=SIZE_CAP statements, default cap 20), - called from inside a loop (a whole-module HIR pre-pass collects callee ids with an in-loop call site — approximates "hot" for an AOT compiler), AND - called from few total sites (default <= 4). The linker raises `-inlinehint-threshold` (default 850) so the hinted kernels actually inline; this lifts LLVM's ceiling ONLY for functions Perry stamped `inlinehint` — every other function keeps the base -O3 threshold, so cold code is untouched. Anti-bloat is the whole point, and it rests on two backstops: 1. The loop gate: a function only ever called from cold/straight-line code is never hinted, so cold utility functions can't bloat the binary. 2. The call-site cap: `-inlinehint-threshold` raises the ceiling at EVERY call site of a hinted callee (LLVM can't tell hot from cold per-site through a function attribute), so a hot kernel also called from many cold sites would be duplicated at all of them. Capping total call sites bounds the duplication. Measured binary-size delta (flag OFF vs ON), byte-precise __text section: - real programs (5 test-files): 0 hints, 0.000% delta; - broadly-called helper (300 fns x 40 cold sites): excluded by cap, 0%; - large programs (>6MB IR compile at -Os): flag inert, 0%; - realistic hot-kernel density: +0.34% (25 kernels), +0.79% (50), +1.57% (100); an all-kernels synthetic (250) reaches +3.86% — bounded by the cap (without it the same program's optimized IR grew 5.7x). `mix` 40M-call microbench (min-of-many, contended box load ~27): OFF 1024ms -> ON 904ms. Structural proof (load-independent): the caller loop no longer emits a `bl` to `mix` (0 vs 2 BR26 relocations), and `mix` carries `inlinehint`. Gated behind PERRY_INLINE_HOT_SMALL (default on); PERRY_INLINE_HOT_SMALL_CAP / _THRESHOLD / _MAX_SITES tune the window, hint threshold, and call-site cap. All four are folded into the object cache key. Existing `noinline` cases (try/setjmp/volatile) are respected via to_ir's has_try-first attribute precedence. Adds test_gap_inline_hot_small.ts (byte-identical to Node ON/OFF and under PERRY_GC_FORCE_EVACUATE=1). Stacks on #6850 (merged). Claude-Session: https://claude.ai/code/session_01UGDwjukzhowLDJYsMPFwMv
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds configurable whole-module analysis for small functions called inside loops, emits LLVM ChangesInline-hot-small code generation
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant HIR
participant collect_hot_loop_callees
participant compile_function
participant LlFunction
participant Clang
HIR->>collect_hot_loop_callees: scan loop-local FuncRef calls
collect_hot_loop_callees->>compile_function: provide hot_loop_callees
compile_function->>LlFunction: mark eligible functions inline_hint
LlFunction->>Clang: emit inlinehint
Clang->>Clang: apply inlinehint-threshold at O3
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 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 |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/perry-codegen/src/collectors/hot_callees.rs`:
- Around line 47-57: Extend the hot-callee scan in the class traversal around
walk_stmts to visit every executable class-member body, including getters,
setters, static methods, computed-member functions, and applicable class
initializer expressions. Ensure all such call sites are recorded before the
existing max_call_sites cap is applied, while preserving the current handling of
constructors and regular methods.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 7973dc7f-b847-4aa2-b25f-bf2105584e83
📒 Files selected for processing (12)
changelog.d/6855-inline-hot-small.mdcrates/perry-codegen/src/codegen/function.rscrates/perry-codegen/src/codegen/helpers.rscrates/perry-codegen/src/codegen/mod.rscrates/perry-codegen/src/codegen/opts.rscrates/perry-codegen/src/collectors/hot_callees.rscrates/perry-codegen/src/collectors/mod.rscrates/perry-codegen/src/function.rscrates/perry-codegen/src/linker.rscrates/perry/src/commands/compile/object_cache.rscrates/perry/src/commands/compile/object_cache/object_cache_tests.rstest-files/test_gap_inline_hot_small.ts
…ensus CodeRabbit (#6855): collect_hot_loop_callees only walked hir.init, free functions, class constructors, and instance methods. It missed static methods, getters/setters, computed-key members (body + key expr), and instance/static field initializers. The miss matters for the anti-bloat cap, not just hot-callee discovery: `max_call_sites` bounds inlinehint duplication, so any call site the census doesn't see is a call site the cap can't count. A small function whose extra call sites hide in a getter or a field initializer could slip under the cap and get hinted despite being widely used, defeating the size backstop. Field/computed-key exprs are walked with in_loop=false so they feed the count without spuriously marking a callee hot. Verified byte-exact vs Node 26.5.0 on a class kernel whose getter, static method, and field initializer each carry an in-loop call (flag on and off).
|
Addressed in f1c14b1.
Field-initializer and computed-key expressions are walked with Verified byte-exact vs Node 26.5.0 on a class kernel whose getter, static method, and field initializer each carry an in-loop call to a small helper (identical output flag-on and flag-off). |
What
Inline small hot (in-loop) functions into their loops — the residual gap a
tight integer-math kernel has vs V8 after #6850 (native
Math.imul+typed-array-param reads) landed. V8 inlines a small hot function into its loop;
Perry left it out-of-line, so the call itself dominated the residual.
Perry already force-inlines functions
<= 8statements withalwaysinline(unconditional). A NaN-boxed bit-mixer like
mixis ~10 statements and costs~800 in LLVM's inline model (GC shadow-frame calls + typed-array reads +
double↔i32 marshaling), well above
-O3's base threshold, so it stayed a call.Bias, don't force. This adds a distinct
inlinehintpath, gated so weonly pay code size where it buys speed:
9..=SIZE_CAPstatements (default cap 20);collectors/hot_callees.rs— collects such callee ids; a function only evercalled from cold/straight-line code is never hinted);
MAX_SITEStotal (default 4).The linker raises LLVM's
-inlinehint-threshold(default 850) so the hintedkernels actually inline. Crucially this lifts the ceiling only for functions
Perry stamped
inlinehint; every other function keeps the base-O3threshold.alwaysinlineis kept for the genuinely tiny (<= 8).noinlinecases(try/setjmp/volatile) are respected via
to_ir'shas_try-first attributeprecedence.
Stacks on #6850 (merged to
main).Why the call-site cap (the anti-bloat backstop)
-inlinehint-thresholdraises the inline ceiling at every call site of ahinted callee (LLVM can't tell hot from cold per-site through a function
attribute). Without the cap, a small kernel that is also called from many cold
sites would be duplicated at all of them. Capping total call sites bounds the
duplication. A synthetic of 300 fns × 40 cold sites, hinted without the cap,
grew its optimized IR 5.7× (205K → 1.17M lines); with the cap those
broadly-called fns are excluded → 0%.
Structural proof (load-independent)
mixcarriesinlinehintin the emitted IR (flag off: no attribute).clang -O3, the caller loop no longer callsmix: 0bl(BR26) relocations to
_perry_fn_..._mixwith the flag on, 2 with it off.Binary-size (flag OFF vs ON, byte-precise
__textsection)-Os)Real programs and broadly-called helpers see zero change. Only a synthetic
composed almost entirely of distinct hot kernels approaches/exceeds the ~2-3%
budget — a shape real code doesn't have — and even that stays bounded (the cap
turns the 5.7× uncapped blowup into ~4%).
Perf
mix40M-call microbench, min-of-7, contended box (load ~27): OFF 1024 ms →ON 904 ms. (A separate run reproduced the prompt's exact OFF baseline of
1862 ms → 1406 ms ON.) Absolutes are load-inflated; the structural proof above
is the load-independent evidence.
Correctness
test-files/test_gap_inline_hot_small.ts(new): a small hot function whoseloop-carried result feeds Int32Array writes — byte-identical to Node 26 with
the flag ON, OFF, and under
PERRY_GC_FORCE_EVACUATE=1.test_gap_*programs: nooutput differences (inlining is semantics-preserving).
object_cacheunit tests updated + green (the 4 new env vars are folded intothe object cache key so a warm cache can't serve a mismatched
.o).cargo test --workspacedelegated to CI (the box wastoo saturated to build
--releaselocally).Flags
PERRY_INLINE_HOT_SMALL(default on) gates the whole feature.PERRY_INLINE_HOT_SMALL_CAP/_THRESHOLD/_MAX_SITEStune the size window,hint threshold, and call-site cap. No version bump / no
CHANGELOG.md(fragmentin
changelog.d/).https://claude.ai/code/session_01UGDwjukzhowLDJYsMPFwMv
Summary by CodeRabbit
Performance Improvements
Reliability
Testing / Configuration