Skip to content

Commit 251a69c

Browse files
committed
Avoid MIR bloat in inlining
In 126578 we ended up with more binary size increases than expected. This change attempts to avoid inlining large things into small things, to avoid that kind of increase, in cases when top-down inlining will still be able to do that inlining later.
1 parent e9e6e2e commit 251a69c

17 files changed

+425
-845
lines changed

compiler/rustc_mir_transform/src/cost_checker.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ const LANDINGPAD_PENALTY: usize = 50;
99
const RESUME_PENALTY: usize = 45;
1010
const LARGE_SWITCH_PENALTY: usize = 20;
1111
const CONST_SWITCH_BONUS: usize = 10;
12+
const MULTIPLE_MUT_PENALTY: usize = 50;
13+
const REF_COPY_BONUS: usize = 5;
14+
const MANY_PARAMETERS_BONUS: usize = 10;
1215

1316
/// Verify that the callee body is compatible with the caller.
1417
#[derive(Clone)]
@@ -31,6 +34,75 @@ impl<'b, 'tcx> CostChecker<'b, 'tcx> {
3134
CostChecker { tcx, param_env, callee_body, instance, penalty: 0, bonus: 0 }
3235
}
3336

37+
/// Add function-level costs not well-represented by the block-level costs.
38+
///
39+
/// Needed because the `CostChecker` is used sometimes for just blocks,
40+
/// and even the full `Inline` doesn't call `visit_body`, so there's nowhere
41+
/// to put this logic in the visitor.
42+
pub fn add_function_level_costs(&mut self) {
43+
// There's usually extra stack costs to passing lots of parameters,
44+
// so we'd rather inline that if the method isn't actually complex,
45+
// especially for trait impls that ignore some parameters.
46+
if self.callee_body.arg_count > 2 {
47+
self.bonus += MANY_PARAMETERS_BONUS;
48+
}
49+
50+
fn is_call_like(bbd: &BasicBlockData<'_>) -> bool {
51+
use TerminatorKind::*;
52+
match bbd.terminator().kind {
53+
Call { .. }
54+
| Drop { .. }
55+
| Assert { .. }
56+
| Yield { .. }
57+
| CoroutineDrop
58+
| InlineAsm { .. } => true,
59+
60+
Goto { .. }
61+
| SwitchInt { .. }
62+
| UnwindResume
63+
| UnwindTerminate(_)
64+
| Return
65+
| Unreachable => false,
66+
67+
FalseEdge { .. } | FalseUnwind { .. } => unreachable!(),
68+
}
69+
}
70+
71+
// If the only has one Call (or similar), inlining isn't increasing the total
72+
// number of calls, so give extra encouragement to inlining that.
73+
if self.callee_body.basic_blocks.iter().filter(|bbd| is_call_like(bbd)).count() == 1 {
74+
self.bonus += CALL_PENALTY;
75+
}
76+
77+
let callee_param_env = std::cell::LazyCell::new(|| {
78+
let def_id = self.callee_body.source.def_id();
79+
self.tcx.param_env(def_id)
80+
});
81+
82+
let mut mut_ref_count = 0;
83+
for local in self.callee_body.args_iter() {
84+
let ty = self.callee_body.local_decls[local].ty;
85+
if let ty::Ref(_, referee, mtbl) = ty.kind() {
86+
match mtbl {
87+
Mutability::Mut => mut_ref_count += 1,
88+
Mutability::Not => {
89+
// Encourage inlining `&impl Copy` parameters,
90+
// as that often lets us remove the indirection.
91+
if referee.is_copy_modulo_regions(self.tcx, *callee_param_env) {
92+
self.bonus += REF_COPY_BONUS;
93+
}
94+
}
95+
}
96+
}
97+
}
98+
99+
// Discourage inlining something with multiple `&mut` parameters,
100+
// as the MIR inliner doesn't know how to preserve `noalias`.
101+
if mut_ref_count > 1 {
102+
self.penalty += MULTIPLE_MUT_PENALTY;
103+
}
104+
}
105+
34106
pub fn cost(&self) -> usize {
35107
usize::saturating_sub(self.penalty, self.bonus)
36108
}

compiler/rustc_mir_transform/src/inline.rs

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,18 @@ fn inline<'tcx>(tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) -> bool {
8585
}
8686

8787
let param_env = tcx.param_env_reveal_all_normalized(def_id);
88+
let codegen_fn_attrs = tcx.codegen_fn_attrs(def_id);
8889

8990
let mut this = Inliner {
9091
tcx,
9192
param_env,
92-
codegen_fn_attrs: tcx.codegen_fn_attrs(def_id),
93+
codegen_fn_attrs,
9394
history: Vec::new(),
9495
changed: false,
96+
caller_is_inline_forwarder: matches!(
97+
codegen_fn_attrs.inline,
98+
InlineAttr::Hint | InlineAttr::Always
99+
) && body_is_forwarder(body),
95100
};
96101
let blocks = START_BLOCK..body.basic_blocks.next_index();
97102
this.process_blocks(body, blocks);
@@ -111,6 +116,9 @@ struct Inliner<'tcx> {
111116
history: Vec<DefId>,
112117
/// Indicates that the caller body has been modified.
113118
changed: bool,
119+
/// Indicates that the caller is #[inline] and just calls another function,
120+
/// and thus we can inline less into it as it'll be inlined itself.
121+
caller_is_inline_forwarder: bool,
114122
}
115123

116124
impl<'tcx> Inliner<'tcx> {
@@ -475,7 +483,9 @@ impl<'tcx> Inliner<'tcx> {
475483
) -> Result<(), &'static str> {
476484
let tcx = self.tcx;
477485

478-
let mut threshold = if cross_crate_inlinable {
486+
let mut threshold = if self.caller_is_inline_forwarder {
487+
self.tcx.sess.opts.unstable_opts.inline_mir_forwarder_threshold.unwrap_or(30)
488+
} else if cross_crate_inlinable {
479489
self.tcx.sess.opts.unstable_opts.inline_mir_hint_threshold.unwrap_or(100)
480490
} else {
481491
self.tcx.sess.opts.unstable_opts.inline_mir_threshold.unwrap_or(50)
@@ -494,6 +504,8 @@ impl<'tcx> Inliner<'tcx> {
494504
let mut checker =
495505
CostChecker::new(self.tcx, self.param_env, Some(callsite.callee), callee_body);
496506

507+
checker.add_function_level_costs();
508+
497509
// Traverse the MIR manually so we can account for the effects of inlining on the CFG.
498510
let mut work_list = vec![START_BLOCK];
499511
let mut visited = BitSet::new_empty(callee_body.basic_blocks.len());
@@ -1081,3 +1093,37 @@ fn try_instance_mir<'tcx>(
10811093
}
10821094
Ok(tcx.instance_mir(instance))
10831095
}
1096+
1097+
fn body_is_forwarder(body: &Body<'_>) -> bool {
1098+
let TerminatorKind::Call { target, .. } = body.basic_blocks[START_BLOCK].terminator().kind
1099+
else {
1100+
return false;
1101+
};
1102+
if let Some(target) = target {
1103+
let TerminatorKind::Return = body.basic_blocks[target].terminator().kind else {
1104+
return false;
1105+
};
1106+
}
1107+
1108+
let max_blocks = if !body.is_polymorphic {
1109+
2
1110+
} else if target.is_none() {
1111+
3
1112+
} else {
1113+
4
1114+
};
1115+
if body.basic_blocks.len() > max_blocks {
1116+
return false;
1117+
}
1118+
1119+
body.basic_blocks.iter_enumerated().all(|(bb, bb_data)| {
1120+
bb == START_BLOCK
1121+
|| matches!(
1122+
bb_data.terminator().kind,
1123+
TerminatorKind::Return
1124+
| TerminatorKind::Drop { .. }
1125+
| TerminatorKind::UnwindResume
1126+
| TerminatorKind::UnwindTerminate(_)
1127+
)
1128+
})
1129+
}

compiler/rustc_session/src/options.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1766,6 +1766,8 @@ options! {
17661766
"enable LLVM inlining (default: yes)"),
17671767
inline_mir: Option<bool> = (None, parse_opt_bool, [TRACKED],
17681768
"enable MIR inlining (default: no)"),
1769+
inline_mir_forwarder_threshold: Option<usize> = (None, parse_opt_number, [TRACKED],
1770+
"inlining threshold when the caller is a simple forwarding function (default: 30)"),
17691771
inline_mir_hint_threshold: Option<usize> = (None, parse_opt_number, [TRACKED],
17701772
"inlining threshold for functions with inline hint (default: 100)"),
17711773
inline_mir_preserve_debug: Option<bool> = (None, parse_opt_bool, [TRACKED],

library/alloc/src/raw_vec.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,7 @@ impl<T, A: Allocator> RawVec<T, A> {
429429
///
430430
/// Aborts on OOM.
431431
#[cfg(not(no_global_oom_handling))]
432+
#[inline]
432433
pub fn shrink_to_fit(&mut self, cap: usize) {
433434
if let Err(err) = self.shrink(cap) {
434435
handle_error(err);
@@ -511,9 +512,25 @@ impl<T, A: Allocator> RawVec<T, A> {
511512
}
512513

513514
#[cfg(not(no_global_oom_handling))]
515+
#[inline]
514516
fn shrink(&mut self, cap: usize) -> Result<(), TryReserveError> {
515517
assert!(cap <= self.capacity(), "Tried to shrink to a larger capacity");
518+
// SAFETY: Just checked this isn't trying to grow
519+
unsafe { self.shrink_unchecked(cap) }
520+
}
516521

522+
/// `shrink`, but without the capacity check.
523+
///
524+
/// This is split out so that `shrink` can inline the check, since it
525+
/// optimizes out in things like `shrink_to_fit`, without needing to
526+
/// also inline all this code, as doing that ends up failing the
527+
/// `vec-shrink-panic` codegen test when `shrink_to_fit` ends up being too
528+
/// big for LLVM to be willing to inline.
529+
///
530+
/// # Safety
531+
/// `cap <= self.capacity()`
532+
#[cfg(not(no_global_oom_handling))]
533+
unsafe fn shrink_unchecked(&mut self, cap: usize) -> Result<(), TryReserveError> {
517534
let (ptr, layout) = if let Some(mem) = self.current_memory() { mem } else { return Ok(()) };
518535
// See current_memory() why this assert is here
519536
const { assert!(mem::size_of::<T>() % mem::align_of::<T>() == 0) };

library/alloc/src/vec/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1101,6 +1101,7 @@ impl<T, A: Allocator> Vec<T, A> {
11011101
/// ```
11021102
#[cfg(not(no_global_oom_handling))]
11031103
#[stable(feature = "rust1", since = "1.0.0")]
1104+
#[inline]
11041105
pub fn shrink_to_fit(&mut self) {
11051106
// The capacity is never less than the length, and there's nothing to do when
11061107
// they are equal, so we can avoid the panic case in `RawVec::shrink_to_fit`
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// MIR for `marked_inline_direct` after Inline
2+
3+
fn marked_inline_direct(_1: i32) -> () {
4+
debug x => _1;
5+
let mut _0: ();
6+
let _2: ();
7+
let mut _3: i32;
8+
9+
bb0: {
10+
StorageLive(_2);
11+
StorageLive(_3);
12+
_3 = _1;
13+
_2 = call_twice(move _3) -> [return: bb1, unwind unreachable];
14+
}
15+
16+
bb1: {
17+
StorageDead(_3);
18+
StorageDead(_2);
19+
_0 = const ();
20+
return;
21+
}
22+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// MIR for `marked_inline_direct` after Inline
2+
3+
fn marked_inline_direct(_1: i32) -> () {
4+
debug x => _1;
5+
let mut _0: ();
6+
let _2: ();
7+
let mut _3: i32;
8+
9+
bb0: {
10+
StorageLive(_2);
11+
StorageLive(_3);
12+
_3 = _1;
13+
_2 = call_twice(move _3) -> [return: bb1, unwind continue];
14+
}
15+
16+
bb1: {
17+
StorageDead(_3);
18+
StorageDead(_2);
19+
_0 = const ();
20+
return;
21+
}
22+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// MIR for `marked_inline_indirect` after Inline
2+
3+
fn marked_inline_indirect(_1: i32) -> () {
4+
debug x => _1;
5+
let mut _0: ();
6+
let _2: ();
7+
let mut _3: i32;
8+
scope 1 (inlined marked_inline_direct) {
9+
let _4: ();
10+
}
11+
12+
bb0: {
13+
StorageLive(_2);
14+
StorageLive(_3);
15+
_3 = _1;
16+
StorageLive(_4);
17+
_4 = call_twice(move _3) -> [return: bb1, unwind unreachable];
18+
}
19+
20+
bb1: {
21+
StorageDead(_4);
22+
StorageDead(_3);
23+
StorageDead(_2);
24+
_0 = const ();
25+
return;
26+
}
27+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// MIR for `marked_inline_indirect` after Inline
2+
3+
fn marked_inline_indirect(_1: i32) -> () {
4+
debug x => _1;
5+
let mut _0: ();
6+
let _2: ();
7+
let mut _3: i32;
8+
scope 1 (inlined marked_inline_direct) {
9+
let _4: ();
10+
}
11+
12+
bb0: {
13+
StorageLive(_2);
14+
StorageLive(_3);
15+
_3 = _1;
16+
StorageLive(_4);
17+
_4 = call_twice(move _3) -> [return: bb1, unwind continue];
18+
}
19+
20+
bb1: {
21+
StorageDead(_4);
22+
StorageDead(_3);
23+
StorageDead(_2);
24+
_0 = const ();
25+
return;
26+
}
27+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// MIR for `monomorphic_not_inline` after Inline
2+
3+
fn monomorphic_not_inline(_1: i32) -> () {
4+
debug x => _1;
5+
let mut _0: ();
6+
let _2: ();
7+
let mut _3: i32;
8+
scope 1 (inlined call_twice) {
9+
let _4: ();
10+
let _5: ();
11+
}
12+
13+
bb0: {
14+
StorageLive(_2);
15+
StorageLive(_3);
16+
_3 = _1;
17+
StorageLive(_4);
18+
StorageLive(_5);
19+
_4 = other_thing(_3) -> [return: bb2, unwind unreachable];
20+
}
21+
22+
bb1: {
23+
StorageDead(_5);
24+
StorageDead(_4);
25+
StorageDead(_3);
26+
StorageDead(_2);
27+
_0 = const ();
28+
return;
29+
}
30+
31+
bb2: {
32+
_5 = other_thing(move _3) -> [return: bb1, unwind unreachable];
33+
}
34+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// MIR for `monomorphic_not_inline` after Inline
2+
3+
fn monomorphic_not_inline(_1: i32) -> () {
4+
debug x => _1;
5+
let mut _0: ();
6+
let _2: ();
7+
let mut _3: i32;
8+
scope 1 (inlined call_twice) {
9+
let _4: ();
10+
let _5: ();
11+
}
12+
13+
bb0: {
14+
StorageLive(_2);
15+
StorageLive(_3);
16+
_3 = _1;
17+
StorageLive(_4);
18+
StorageLive(_5);
19+
_4 = other_thing(_3) -> [return: bb2, unwind continue];
20+
}
21+
22+
bb1: {
23+
StorageDead(_5);
24+
StorageDead(_4);
25+
StorageDead(_3);
26+
StorageDead(_2);
27+
_0 = const ();
28+
return;
29+
}
30+
31+
bb2: {
32+
_5 = other_thing(move _3) -> [return: bb1, unwind continue];
33+
}
34+
}

0 commit comments

Comments
 (0)