Skip to content

Commit ee1fedf

Browse files
authored
Rollup merge of #78580 - tmiasko:inline-loop, r=oli-obk
inliner: Break inlining cycles Keep track of all instances inlined so far. When examining a new call sites from an inlined body, skip those where callee had been inlined already to avoid potential inlining cycles. Fixes #78573.
2 parents 7ac079f + dc4d74d commit ee1fedf

File tree

4 files changed

+248
-123
lines changed

4 files changed

+248
-123
lines changed

compiler/rustc_mir/src/transform/inline.rs

+114-123
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Inlining pass for MIR functions
22
33
use rustc_attr as attr;
4+
use rustc_hir as hir;
45
use rustc_index::bit_set::BitSet;
56
use rustc_index::vec::Idx;
67
use rustc_middle::middle::codegen_fn_attrs::{CodegenFnAttrFlags, CodegenFnAttrs};
@@ -12,9 +13,8 @@ use rustc_target::spec::abi::Abi;
1213

1314
use super::simplify::{remove_dead_blocks, CfgSimplifier};
1415
use crate::transform::MirPass;
15-
use std::collections::VecDeque;
1616
use std::iter;
17-
use std::ops::RangeFrom;
17+
use std::ops::{Range, RangeFrom};
1818

1919
const DEFAULT_THRESHOLD: usize = 50;
2020
const HINT_THRESHOLD: usize = 100;
@@ -37,132 +37,128 @@ struct CallSite<'tcx> {
3737

3838
impl<'tcx> MirPass<'tcx> for Inline {
3939
fn run_pass(&self, tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
40-
if tcx.sess.opts.debugging_opts.mir_opt_level >= 2 {
41-
if tcx.sess.opts.debugging_opts.instrument_coverage {
42-
// The current implementation of source code coverage injects code region counters
43-
// into the MIR, and assumes a 1-to-1 correspondence between MIR and source-code-
44-
// based function.
45-
debug!("function inlining is disabled when compiling with `instrument_coverage`");
46-
} else {
47-
Inliner {
48-
tcx,
49-
param_env: tcx.param_env_reveal_all_normalized(body.source.def_id()),
50-
codegen_fn_attrs: tcx.codegen_fn_attrs(body.source.def_id()),
51-
}
52-
.run_pass(body);
53-
}
40+
if tcx.sess.opts.debugging_opts.mir_opt_level < 2 {
41+
return;
42+
}
43+
44+
if tcx.sess.opts.debugging_opts.instrument_coverage {
45+
// The current implementation of source code coverage injects code region counters
46+
// into the MIR, and assumes a 1-to-1 correspondence between MIR and source-code-
47+
// based function.
48+
debug!("function inlining is disabled when compiling with `instrument_coverage`");
49+
return;
50+
}
51+
52+
if inline(tcx, body) {
53+
debug!("running simplify cfg on {:?}", body.source);
54+
CfgSimplifier::new(body).simplify();
55+
remove_dead_blocks(body);
5456
}
5557
}
5658
}
5759

60+
fn inline(tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) -> bool {
61+
let def_id = body.source.def_id();
62+
let hir_id = tcx.hir().local_def_id_to_hir_id(def_id.expect_local());
63+
64+
// Only do inlining into fn bodies.
65+
if !tcx.hir().body_owner_kind(hir_id).is_fn_or_closure() {
66+
return false;
67+
}
68+
if body.source.promoted.is_some() {
69+
return false;
70+
}
71+
72+
let mut this = Inliner {
73+
tcx,
74+
param_env: tcx.param_env_reveal_all_normalized(body.source.def_id()),
75+
codegen_fn_attrs: tcx.codegen_fn_attrs(body.source.def_id()),
76+
hir_id,
77+
history: Vec::new(),
78+
changed: false,
79+
};
80+
let blocks = BasicBlock::new(0)..body.basic_blocks().next_index();
81+
this.process_blocks(body, blocks);
82+
this.changed
83+
}
84+
5885
struct Inliner<'tcx> {
5986
tcx: TyCtxt<'tcx>,
6087
param_env: ParamEnv<'tcx>,
88+
/// Caller codegen attributes.
6189
codegen_fn_attrs: &'tcx CodegenFnAttrs,
90+
/// Caller HirID.
91+
hir_id: hir::HirId,
92+
/// Stack of inlined instances.
93+
history: Vec<Instance<'tcx>>,
94+
/// Indicates that the caller body has been modified.
95+
changed: bool,
6296
}
6397

6498
impl Inliner<'tcx> {
65-
fn run_pass(&self, caller_body: &mut Body<'tcx>) {
66-
// Keep a queue of callsites to try inlining on. We take
67-
// advantage of the fact that queries detect cycles here to
68-
// allow us to try and fetch the fully optimized MIR of a
69-
// call; if it succeeds, we can inline it and we know that
70-
// they do not call us. Otherwise, we just don't try to
71-
// inline.
72-
//
73-
// We use a queue so that we inline "broadly" before we inline
74-
// in depth. It is unclear if this is the best heuristic,
75-
// really, but that's true of all the heuristics in this
76-
// file. =)
77-
78-
let mut callsites = VecDeque::new();
79-
80-
let def_id = caller_body.source.def_id();
81-
82-
// Only do inlining into fn bodies.
83-
let self_hir_id = self.tcx.hir().local_def_id_to_hir_id(def_id.expect_local());
84-
if self.tcx.hir().body_owner_kind(self_hir_id).is_fn_or_closure()
85-
&& caller_body.source.promoted.is_none()
86-
{
87-
for (bb, bb_data) in caller_body.basic_blocks().iter_enumerated() {
88-
if let Some(callsite) = self.get_valid_function_call(bb, bb_data, caller_body) {
89-
callsites.push_back(callsite);
90-
}
91-
}
92-
} else {
93-
return;
94-
}
95-
96-
let mut changed = false;
97-
while let Some(callsite) = callsites.pop_front() {
98-
debug!("checking whether to inline callsite {:?}", callsite);
99+
fn process_blocks(&mut self, caller_body: &mut Body<'tcx>, blocks: Range<BasicBlock>) {
100+
for bb in blocks {
101+
let callsite = match self.get_valid_function_call(bb, &caller_body[bb], caller_body) {
102+
None => continue,
103+
Some(it) => it,
104+
};
99105

100-
if let InstanceDef::Item(_) = callsite.callee.def {
101-
if !self.tcx.is_mir_available(callsite.callee.def_id()) {
102-
debug!("checking whether to inline callsite {:?} - MIR unavailable", callsite,);
103-
continue;
104-
}
106+
if !self.is_mir_available(&callsite.callee, caller_body) {
107+
debug!("MIR unavailable {}", callsite.callee);
108+
continue;
105109
}
106110

107-
let callee_body = if let Some(callee_def_id) = callsite.callee.def_id().as_local() {
108-
let callee_hir_id = self.tcx.hir().local_def_id_to_hir_id(callee_def_id);
109-
// Avoid a cycle here by only using `instance_mir` only if we have
110-
// a lower `HirId` than the callee. This ensures that the callee will
111-
// not inline us. This trick only works without incremental compilation.
112-
// So don't do it if that is enabled. Also avoid inlining into generators,
113-
// since their `optimized_mir` is used for layout computation, which can
114-
// create a cycle, even when no attempt is made to inline the function
115-
// in the other direction.
116-
if !self.tcx.dep_graph.is_fully_enabled()
117-
&& self_hir_id < callee_hir_id
118-
&& caller_body.generator_kind.is_none()
119-
{
120-
self.tcx.instance_mir(callsite.callee.def)
121-
} else {
122-
continue;
123-
}
124-
} else {
125-
// This cannot result in a cycle since the callee MIR is from another crate
126-
// and is already optimized.
127-
self.tcx.instance_mir(callsite.callee.def)
128-
};
129-
130-
if !self.consider_optimizing(callsite, &callee_body) {
111+
let callee_body = self.tcx.instance_mir(callsite.callee.def);
112+
if !self.should_inline(callsite, callee_body) {
131113
continue;
132114
}
133115

116+
if !self.tcx.consider_optimizing(|| {
117+
format!("Inline {:?} into {}", callee_body.span, callsite.callee)
118+
}) {
119+
return;
120+
}
121+
134122
let callee_body = callsite.callee.subst_mir_and_normalize_erasing_regions(
135123
self.tcx,
136124
self.param_env,
137125
callee_body,
138126
);
139127

140-
let start = caller_body.basic_blocks().len();
141-
debug!("attempting to inline callsite {:?} - body={:?}", callsite, callee_body);
142-
if !self.inline_call(callsite, caller_body, callee_body) {
143-
debug!("attempting to inline callsite {:?} - failure", callsite);
144-
continue;
145-
}
146-
debug!("attempting to inline callsite {:?} - success", callsite);
147-
148-
// Add callsites from inlined function
149-
for (bb, bb_data) in caller_body.basic_blocks().iter_enumerated().skip(start) {
150-
if let Some(new_callsite) = self.get_valid_function_call(bb, bb_data, caller_body) {
151-
// Don't inline the same function multiple times.
152-
if callsite.callee != new_callsite.callee {
153-
callsites.push_back(new_callsite);
154-
}
155-
}
156-
}
128+
let old_blocks = caller_body.basic_blocks().next_index();
129+
self.inline_call(callsite, caller_body, callee_body);
130+
let new_blocks = old_blocks..caller_body.basic_blocks().next_index();
131+
self.changed = true;
132+
133+
self.history.push(callsite.callee);
134+
self.process_blocks(caller_body, new_blocks);
135+
self.history.pop();
136+
}
137+
}
157138

158-
changed = true;
139+
fn is_mir_available(&self, callee: &Instance<'tcx>, caller_body: &Body<'tcx>) -> bool {
140+
if let InstanceDef::Item(_) = callee.def {
141+
if !self.tcx.is_mir_available(callee.def_id()) {
142+
return false;
143+
}
159144
}
160145

161-
// Simplify if we inlined anything.
162-
if changed {
163-
debug!("running simplify cfg on {:?}", caller_body.source);
164-
CfgSimplifier::new(caller_body).simplify();
165-
remove_dead_blocks(caller_body);
146+
if let Some(callee_def_id) = callee.def_id().as_local() {
147+
let callee_hir_id = self.tcx.hir().local_def_id_to_hir_id(callee_def_id);
148+
// Avoid a cycle here by only using `instance_mir` only if we have
149+
// a lower `HirId` than the callee. This ensures that the callee will
150+
// not inline us. This trick only works without incremental compilation.
151+
// So don't do it if that is enabled. Also avoid inlining into generators,
152+
// since their `optimized_mir` is used for layout computation, which can
153+
// create a cycle, even when no attempt is made to inline the function
154+
// in the other direction.
155+
!self.tcx.dep_graph.is_fully_enabled()
156+
&& self.hir_id < callee_hir_id
157+
&& caller_body.generator_kind.is_none()
158+
} else {
159+
// This cannot result in a cycle since the callee MIR is from another crate
160+
// and is already optimized.
161+
true
166162
}
167163
}
168164

@@ -179,7 +175,8 @@ impl Inliner<'tcx> {
179175

180176
// Only consider direct calls to functions
181177
let terminator = bb_data.terminator();
182-
if let TerminatorKind::Call { func: ref op, .. } = terminator.kind {
178+
// FIXME: Handle inlining of diverging calls
179+
if let TerminatorKind::Call { func: ref op, destination: Some(_), .. } = terminator.kind {
183180
if let ty::FnDef(callee_def_id, substs) = *op.ty(caller_body, self.tcx).kind() {
184181
// To resolve an instance its substs have to be fully normalized, so
185182
// we do this here.
@@ -200,14 +197,6 @@ impl Inliner<'tcx> {
200197
None
201198
}
202199

203-
fn consider_optimizing(&self, callsite: CallSite<'tcx>, callee_body: &Body<'tcx>) -> bool {
204-
debug!("consider_optimizing({:?})", callsite);
205-
self.should_inline(callsite, callee_body)
206-
&& self.tcx.consider_optimizing(|| {
207-
format!("Inline {:?} into {:?}", callee_body.span, callsite)
208-
})
209-
}
210-
211200
fn should_inline(&self, callsite: CallSite<'tcx>, callee_body: &Body<'tcx>) -> bool {
212201
debug!("should_inline({:?})", callsite);
213202
let tcx = self.tcx;
@@ -327,7 +316,18 @@ impl Inliner<'tcx> {
327316
}
328317

329318
TerminatorKind::Call { func: Operand::Constant(ref f), cleanup, .. } => {
330-
if let ty::FnDef(def_id, _) = *f.literal.ty.kind() {
319+
if let ty::FnDef(def_id, substs) =
320+
*callsite.callee.subst_mir(self.tcx, &f.literal.ty).kind()
321+
{
322+
let substs = self.tcx.normalize_erasing_regions(self.param_env, substs);
323+
if let Ok(Some(instance)) =
324+
Instance::resolve(self.tcx, self.param_env, def_id, substs)
325+
{
326+
if callsite.callee == instance || self.history.contains(&instance) {
327+
debug!("`callee is recursive - not inlining");
328+
return false;
329+
}
330+
}
331331
// Don't give intrinsics the extra penalty for calls
332332
let f = tcx.fn_sig(def_id);
333333
if f.abi() == Abi::RustIntrinsic || f.abi() == Abi::PlatformIntrinsic {
@@ -397,13 +397,10 @@ impl Inliner<'tcx> {
397397
callsite: CallSite<'tcx>,
398398
caller_body: &mut Body<'tcx>,
399399
mut callee_body: Body<'tcx>,
400-
) -> bool {
400+
) {
401401
let terminator = caller_body[callsite.bb].terminator.take().unwrap();
402402
match terminator.kind {
403-
// FIXME: Handle inlining of diverging calls
404403
TerminatorKind::Call { args, destination: Some(destination), cleanup, .. } => {
405-
debug!("inlined {:?} into {:?}", callsite.callee, caller_body.source);
406-
407404
// If the call is something like `a[*i] = f(i)`, where
408405
// `i : &mut usize`, then just duplicating the `a[*i]`
409406
// Place could result in two different locations if `f`
@@ -519,14 +516,8 @@ impl Inliner<'tcx> {
519516
matches!(constant.literal.val, ConstKind::Unevaluated(_, _, _))
520517
}),
521518
);
522-
523-
true
524-
}
525-
kind => {
526-
caller_body[callsite.bb].terminator =
527-
Some(Terminator { source_info: terminator.source_info, kind });
528-
false
529519
}
520+
kind => bug!("unexpected terminator kind {:?}", kind),
530521
}
531522
}
532523

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Check that inliner handles various forms of recursion and doesn't fall into
2+
// an infinite inlining cycle. The particular outcome of inlining is not
3+
// crucial otherwise.
4+
//
5+
// Regression test for issue #78573.
6+
7+
fn main() {
8+
one();
9+
two();
10+
}
11+
12+
// EMIT_MIR inline_cycle.one.Inline.diff
13+
fn one() {
14+
<C as Call>::call();
15+
}
16+
17+
pub trait Call {
18+
fn call();
19+
}
20+
21+
pub struct A<T>(T);
22+
pub struct B<T>(T);
23+
pub struct C;
24+
25+
impl<T: Call> Call for A<T> {
26+
#[inline]
27+
fn call() {
28+
<B<T> as Call>::call()
29+
}
30+
}
31+
32+
33+
impl<T: Call> Call for B<T> {
34+
#[inline]
35+
fn call() {
36+
<T as Call>::call()
37+
}
38+
}
39+
40+
impl Call for C {
41+
#[inline]
42+
fn call() {
43+
A::<C>::call()
44+
}
45+
}
46+
47+
// EMIT_MIR inline_cycle.two.Inline.diff
48+
fn two() {
49+
call(f);
50+
}
51+
52+
#[inline]
53+
fn call<F: FnOnce()>(f: F) {
54+
f();
55+
}
56+
57+
#[inline]
58+
fn f() {
59+
call(f);
60+
}

0 commit comments

Comments
 (0)