Skip to content

[perf] Allow cross-crate inlining through explicitly inline trait calls - #159119

Open
obi1kenobi wants to merge 1 commit into
rust-lang:mainfrom
obi1kenobi:pg/cross-crate-inlining
Open

[perf] Allow cross-crate inlining through explicitly inline trait calls#159119
obi1kenobi wants to merge 1 commit into
rust-lang:mainfrom
obi1kenobi:pg/cross-crate-inlining

Conversation

@obi1kenobi

@obi1kenobi obi1kenobi commented Jul 11, 2026

Copy link
Copy Markdown
Member

View all comments

Inferred cross-crate inlining currently makes an otherwise-unannotated function available to downstream crates only when its optimized MIR is small and contains no ordinary calls, with intrinsic calls exempted.

This misses small functions whose remaining calls are statically dispatched trait calls to implementations explicitly marked #[inline], since the MIR call identifies the trait item but the relevant inline attribute belongs to the implementation selected for that call.

This PR addresses that edge case by resolving the selected implementation during cross-crate inlining cost analysis. When resolution produces a statically selected, explicitly inline item that is not externally exported, the call no longer disqualifies the enclosing function.

Motivation

The nom-json runtime benchmark uses nom 7.1.3, and its hot path includes an unannotated implementation equivalent to:

impl FindToken<char> for &str {
    fn find_token(&self, token: char) -> bool {
        self.chars().any(|character| character == token)
    }
}

The iterator operations here are marked #[inline], but prior to this PR that wasn't sufficient β€” find_token() was not made available for downstream cross-crate inlining.

The benchmarked function uses the JSON parser and repeatedly uses find_token() to look for ". Making find_token() available for downstream inlining allows constant propagation to replace the general-case search with a direct comparison, which is much more efficient.

The same optimization already happens in LTO-enabled builds. However, LTO is much more expensive than regular cargo build --release. This PR recovers that optimization for regular cargo build --release builds.

The same optimization can also be recovered by marking find_token() with the #[inline] attribute. While that's still probably a reasonable idea, I still think it's a good idea to offer better out-of-the-box performance for default (non-LTO) release builds even when users haven't realized that their small cross-crate trait function implementation is a good #[inline] candidate.

r? oli-obk

AI disclosure: The optimization opportunity here was discovered as part of a systematic probe for missed optimizations using a combination of both traditional and AI tools. The code here was initially prototyped and vetted by AI tools, followed by additional manual work. I secured approval in advance from the reviewer I pinged. I stand behind the quality of the code I'm submitting, and I vouch it's as good or better compared to if I had written every line by my own hand.

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Jul 11, 2026
@Kobzol

Kobzol commented Jul 11, 2026

Copy link
Copy Markdown
Member

@bors try @rust-timer queue

@rust-timer

This comment has been minimized.

@rustbot rustbot added the S-waiting-on-perf Status: Waiting on a perf run to be completed. label Jul 11, 2026
@rust-bors

This comment has been minimized.

rust-bors Bot pushed a commit that referenced this pull request Jul 11, 2026
[perf] Enable inlining trait items that only contain inlined code.
@rust-bors

rust-bors Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

β˜€οΈ Try build successful (CI)
Build commit: 20bd965 (20bd9651565dfb3135d6e60029dfd8f975ff5329)
Base parent: 48e8ec6 (48e8ec6f05057f942a97187dde770a118e05f42c)

@rust-timer

This comment has been minimized.

@rust-timer

Copy link
Copy Markdown
Collaborator

Finished benchmarking commit (20bd965): comparison URL.

Overall result: βŒβœ… regressions and improvements - please read:

Benchmarking means the PR may be perf-sensitive. It's automatically marked not fit for rolling up. Overriding is possible but disadvised: it risks changing compiler perf.

Next, please: If you can, justify the regressions found in this try perf run in writing along with @rustbot label: +perf-regression-triaged. If not, fix the regressions and do another perf run. Neutral or positive results will clear the label automatically.

@bors rollup=never
@rustbot label: -S-waiting-on-perf +perf-regression

Instruction count

Our most reliable metric. Used to determine the overall result above. However, even this metric can be noisy.

mean range count
Regressions ❌
(primary)
0.5% [0.2%, 1.7%] 17
Regressions ❌
(secondary)
0.4% [0.0%, 0.7%] 29
Improvements βœ…
(primary)
-1.7% [-6.3%, -0.3%] 5
Improvements βœ…
(secondary)
-0.3% [-4.3%, -0.0%] 31
All βŒβœ… (primary) -0.0% [-6.3%, 1.7%] 22

Max RSS (memory usage)

Results (primary 0.9%, secondary 1.1%)

A less reliable metric. May be of interest, but not used to determine the overall result above.

mean range count
Regressions ❌
(primary)
3.0% [2.7%, 3.3%] 2
Regressions ❌
(secondary)
3.1% [3.1%, 3.1%] 1
Improvements βœ…
(primary)
-3.2% [-3.2%, -3.2%] 1
Improvements βœ…
(secondary)
-0.9% [-0.9%, -0.9%] 1
All βŒβœ… (primary) 0.9% [-3.2%, 3.3%] 3

Cycles

Results (primary -7.0%, secondary 5.4%)

A less reliable metric. May be of interest, but not used to determine the overall result above.

mean range count
Regressions ❌
(primary)
- - 0
Regressions ❌
(secondary)
10.1% [1.8%, 19.7%] 11
Improvements βœ…
(primary)
-7.0% [-7.0%, -7.0%] 1
Improvements βœ…
(secondary)
-3.3% [-5.4%, -2.0%] 6
All βŒβœ… (primary) -7.0% [-7.0%, -7.0%] 1

Binary size

Results (primary -0.0%, secondary 0.1%)

A less reliable metric. May be of interest, but not used to determine the overall result above.

mean range count
Regressions ❌
(primary)
0.5% [0.1%, 0.7%] 4
Regressions ❌
(secondary)
0.3% [0.1%, 0.4%] 2
Improvements βœ…
(primary)
-0.3% [-1.5%, -0.0%] 9
Improvements βœ…
(secondary)
-0.3% [-0.3%, -0.3%] 1
All βŒβœ… (primary) -0.0% [-1.5%, 0.7%] 13

Bootstrap: 490.729s -> 491.057s (0.07%)
Artifact size: 389.29 MiB -> 389.83 MiB (0.14%)

@rustbot rustbot added perf-regression Performance regression. and removed S-waiting-on-perf Status: Waiting on a perf run to be completed. labels Jul 11, 2026
@Kobzol

Kobzol commented Jul 11, 2026

Copy link
Copy Markdown
Member

@obi1kenobi
obi1kenobi force-pushed the pg/cross-crate-inlining branch from 75fddd9 to 8ba0090 Compare August 3, 2026 06:15
@nnethercote

Copy link
Copy Markdown
Contributor

@bors try @rust-timer queue

@rust-timer

This comment has been minimized.

@rustbot rustbot added the S-waiting-on-perf Status: Waiting on a perf run to be completed. label Aug 3, 2026
@rust-bors

This comment has been minimized.

rust-bors Bot pushed a commit that referenced this pull request Aug 3, 2026
[perf] Enable inlining trait items that only contain inlined code.
Comment on lines +181 to +185
match codegen_fn_attrs.inline {
InlineAttr::Always | InlineAttr::Force { .. } => true,
InlineAttr::Hint => !matches!(tcx.sess.opts.optimize, OptLevel::No),
InlineAttr::None | InlineAttr::Never => false,
}

@obi1kenobi obi1kenobi Aug 3, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is probably worth considering more closely. Should we keep the behavior the same in both optimized and unoptimized builds?

If yes, I'm happy to change this.

If no, then perhaps we should consider not bothering to resolve the trait fn call in unoptimized builds, and instead add an early-exit for that case near the top of this function.

Feedback welcome!

View changes since the review

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

hm. Ideally we'd keep it entirely in sync with what cross_crate_inlineable does. We've seen very nice compile-time improvements in debug mode due to inlining.

let's do a perf run with this logic entirely deduplicated with cross_crate_inlineable, (and done at the top both of this function and cross_crate_inlineable

@obi1kenobi
obi1kenobi marked this pull request as ready for review August 3, 2026 07:05
@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Aug 3, 2026
@rustbot

rustbot commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Some changes occurred to MIR optimizations

cc @rust-lang/wg-mir-opt

@rustbot rustbot removed the S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. label Aug 3, 2026
@obi1kenobi obi1kenobi changed the title [perf] Enable inlining trait items that only contain inlined code. [perf] Allow cross-crate inlining through explicitly inline trait calls Aug 3, 2026
@rust-bors

rust-bors Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

β˜€οΈ Try build successful (CI)
Build commit: b146532 (b146532349830911b317d35346adeaff0bc0478d)
Base parent: 65dd30f (65dd30fb9e882a7e8f0be10caca62936db2a98b8)

@rust-timer

This comment has been minimized.

@rust-timer

Copy link
Copy Markdown
Collaborator

Finished benchmarking commit (b146532): comparison URL.

Overall result: βŒβœ… regressions and improvements - please read:

Benchmarking means the PR may be perf-sensitive. It's automatically marked not fit for rolling up. Overriding is possible but disadvised: it risks changing compiler perf.

Next, please: If you can, justify the regressions found in this try perf run in writing along with @rustbot label: +perf-regression-triaged. If not, fix the regressions and do another perf run. Neutral or positive results will clear the label automatically.

@bors rollup=never rustc-perf
@rustbot label: -S-waiting-on-perf +perf-regression

Instruction count

Our most reliable metric. Used to determine the overall result above. However, even this metric can be noisy.

mean range count
Regressions ❌
(primary)
0.7% [0.3%, 1.6%] 5
Regressions ❌
(secondary)
0.4% [0.3%, 0.4%] 2
Improvements βœ…
(primary)
-1.3% [-6.2%, -0.3%] 7
Improvements βœ…
(secondary)
-0.6% [-4.3%, -0.2%] 12
All βŒβœ… (primary) -0.5% [-6.2%, 1.6%] 12

Max RSS (memory usage)

Results (primary 1.0%, secondary -0.2%)

A less reliable metric. May be of interest, but not used to determine the overall result above.

mean range count
Regressions ❌
(primary)
4.0% [2.2%, 6.3%] 3
Regressions ❌
(secondary)
2.5% [2.5%, 2.5%] 1
Improvements βœ…
(primary)
-3.4% [-4.1%, -2.8%] 2
Improvements βœ…
(secondary)
-0.6% [-0.9%, -0.5%] 8
All βŒβœ… (primary) 1.0% [-4.1%, 6.3%] 5

Cycles

Results (primary -0.7%, secondary -0.5%)

A less reliable metric. May be of interest, but not used to determine the overall result above.

mean range count
Regressions ❌
(primary)
0.7% [0.5%, 1.1%] 4
Regressions ❌
(secondary)
1.9% [0.5%, 3.7%] 5
Improvements βœ…
(primary)
-2.6% [-6.3%, -0.5%] 3
Improvements βœ…
(secondary)
-1.9% [-4.8%, -0.5%] 9
All βŒβœ… (primary) -0.7% [-6.3%, 1.1%] 7

Binary size

Results (primary -0.1%, secondary -0.5%)

A less reliable metric. May be of interest, but not used to determine the overall result above.

mean range count
Regressions ❌
(primary)
0.3% [0.1%, 0.9%] 14
Regressions ❌
(secondary)
0.3% [0.1%, 0.4%] 2
Improvements βœ…
(primary)
-0.3% [-2.3%, -0.0%] 29
Improvements βœ…
(secondary)
-0.5% [-0.6%, -0.0%] 52
All βŒβœ… (primary) -0.1% [-2.3%, 0.9%] 43

Bootstrap: 491.898s -> 491.326s (-0.12%)
Artifact size: 391.08 MiB -> 390.33 MiB (-0.19%)

@rustbot rustbot removed the S-waiting-on-perf Status: Waiting on a perf run to be completed. label Aug 3, 2026
// if we haven't already given up (or are about to give up)
// on cross-crate inlining due to other disqualifying findings.
if self.cost_allows_cross_crate_inlining()
&& !matches!(unwind, UnwindAction::Cleanup(_))

@oli-obk oli-obk Aug 6, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

document why we don't do this for calls which can unwind

View changes since the review

return false;
};

let InstanceKind::Item(_) = instance.def else {

@oli-obk oli-obk Aug 6, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

what else can it be if it's a trait method and got successfully resolved to an actual instance?

View changes since the review


let typing_env = self.typing_env();
let Some((instance, _)) =
inline::try_resolve_call_instance(tcx, typing_env, def_id, ty::Binder::dummy(args))

@oli-obk oli-obk Aug 6, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

cc @addiesh this is unreachable under your work because the const_fn_def call changed in visit_terminator below will ICE. Unless... this late in the MIR pipeline, do we even have any late bound vars left? it's not like they matter anymore and already all got resolved properly in borrowck

View changes since the review

Comment on lines +181 to +185
match codegen_fn_attrs.inline {
InlineAttr::Always | InlineAttr::Force { .. } => true,
InlineAttr::Hint => !matches!(tcx.sess.opts.optimize, OptLevel::No),
InlineAttr::None | InlineAttr::Never => false,
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

hm. Ideally we'd keep it entirely in sync with what cross_crate_inlineable does. We've seen very nice compile-time improvements in debug mode due to inlining.

let's do a perf run with this logic entirely deduplicated with cross_crate_inlineable, (and done at the top both of this function and cross_crate_inlineable

Comment on lines -106 to +115
checker.calls == 0
&& checker.resumes == 0
&& checker.landing_pads == 0
&& checker.statements <= threshold

checker.cost_allows_cross_crate_inlining()

@oli-obk oli-obk Aug 6, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Make this a separate commit before the other changes in this PR

View changes since the review

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Aug 6, 2026
@@ -0,0 +1,74 @@
//@ compile-flags: -Copt-level=3 -Zinline-mir=no -Zcross-crate-inline-threshold=100

@saethlin saethlin Aug 6, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think it is a good idea to pin down the threshold in a test. You might think that makes the test less flaky, but actually it means we change from testing how rustc in the wild optimizes code to whether this specific heuristic works as intended, in its own private universe.

View changes since the review

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

Labels

perf-regression Performance regression. S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants