Skip to content

Commit ee4129f

Browse files
Rollup merge of #82491 - tmiasko:i, r=lcnr
Consider inexpensive inlining criteria first Refactor inlining decisions so that inexpensive criteria are considered first: 1. Based on code generation attributes. 2. Based on MIR availability (examines call graph). 3. Based on MIR body.
2 parents 039b1b6 + 6d5c0c1 commit ee4129f

File tree

1 file changed

+146
-124
lines changed

1 file changed

+146
-124
lines changed

compiler/rustc_mir/src/transform/inline.rs

+146-124
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Inlining pass for MIR functions
22
3-
use rustc_attr as attr;
3+
use rustc_attr::InlineAttr;
44
use rustc_hir as hir;
55
use rustc_index::bit_set::BitSet;
66
use rustc_index::vec::Idx;
@@ -106,72 +106,90 @@ struct Inliner<'tcx> {
106106
impl Inliner<'tcx> {
107107
fn process_blocks(&mut self, caller_body: &mut Body<'tcx>, blocks: Range<BasicBlock>) {
108108
for bb in blocks {
109-
let callsite = match self.get_valid_function_call(bb, &caller_body[bb], caller_body) {
109+
let bb_data = &caller_body[bb];
110+
if bb_data.is_cleanup {
111+
continue;
112+
}
113+
114+
let callsite = match self.resolve_callsite(caller_body, bb, bb_data) {
110115
None => continue,
111116
Some(it) => it,
112117
};
118+
113119
let span = trace_span!("process_blocks", %callsite.callee, ?bb);
114120
let _guard = span.enter();
115121

116-
trace!(
117-
"checking for self recursion ({:?} vs body_source: {:?})",
118-
callsite.callee.def_id(),
119-
caller_body.source.def_id()
120-
);
121-
if callsite.callee.def_id() == caller_body.source.def_id() {
122-
debug!("Not inlining a function into itself");
123-
continue;
124-
}
125-
126-
if !self.is_mir_available(callsite.callee, caller_body) {
127-
debug!("MIR unavailable {}", callsite.callee);
128-
continue;
122+
match self.try_inlining(caller_body, &callsite) {
123+
Err(reason) => {
124+
debug!("not-inlined {} [{}]", callsite.callee, reason);
125+
continue;
126+
}
127+
Ok(new_blocks) => {
128+
debug!("inlined {}", callsite.callee);
129+
self.changed = true;
130+
self.history.push(callsite.callee);
131+
self.process_blocks(caller_body, new_blocks);
132+
self.history.pop();
133+
}
129134
}
135+
}
136+
}
130137

131-
let span = trace_span!("instance_mir", %callsite.callee);
132-
let instance_mir_guard = span.enter();
133-
let callee_body = self.tcx.instance_mir(callsite.callee.def);
134-
drop(instance_mir_guard);
135-
if !self.should_inline(callsite, callee_body) {
136-
continue;
137-
}
138+
/// Attempts to inline a callsite into the caller body. When successful returns basic blocks
139+
/// containing the inlined body. Otherwise returns an error describing why inlining didn't take
140+
/// place.
141+
fn try_inlining(
142+
&self,
143+
caller_body: &mut Body<'tcx>,
144+
callsite: &CallSite<'tcx>,
145+
) -> Result<std::ops::Range<BasicBlock>, &'static str> {
146+
let callee_attrs = self.tcx.codegen_fn_attrs(callsite.callee.def_id());
147+
self.check_codegen_attributes(callsite, callee_attrs)?;
148+
self.check_mir_is_available(caller_body, &callsite.callee)?;
149+
let callee_body = self.tcx.instance_mir(callsite.callee.def);
150+
self.check_mir_body(callsite, callee_body, callee_attrs)?;
151+
152+
if !self.tcx.consider_optimizing(|| {
153+
format!("Inline {:?} into {}", callee_body.span, callsite.callee)
154+
}) {
155+
return Err("optimization fuel exhausted");
156+
}
138157

139-
if !self.tcx.consider_optimizing(|| {
140-
format!("Inline {:?} into {}", callee_body.span, callsite.callee)
141-
}) {
142-
return;
143-
}
158+
let callee_body = callsite.callee.subst_mir_and_normalize_erasing_regions(
159+
self.tcx,
160+
self.param_env,
161+
callee_body.clone(),
162+
);
144163

145-
let callee_body = callsite.callee.subst_mir_and_normalize_erasing_regions(
146-
self.tcx,
147-
self.param_env,
148-
callee_body.clone(),
149-
);
164+
let old_blocks = caller_body.basic_blocks().next_index();
165+
self.inline_call(caller_body, &callsite, callee_body);
166+
let new_blocks = old_blocks..caller_body.basic_blocks().next_index();
150167

151-
let old_blocks = caller_body.basic_blocks().next_index();
152-
self.inline_call(callsite, caller_body, callee_body);
153-
let new_blocks = old_blocks..caller_body.basic_blocks().next_index();
154-
self.changed = true;
168+
Ok(new_blocks)
169+
}
155170

156-
self.history.push(callsite.callee);
157-
self.process_blocks(caller_body, new_blocks);
158-
self.history.pop();
171+
fn check_mir_is_available(
172+
&self,
173+
caller_body: &Body<'tcx>,
174+
callee: &Instance<'tcx>,
175+
) -> Result<(), &'static str> {
176+
if callee.def_id() == caller_body.source.def_id() {
177+
return Err("self-recursion");
159178
}
160-
}
161179

162-
#[instrument(level = "debug", skip(self, caller_body))]
163-
fn is_mir_available(&self, callee: Instance<'tcx>, caller_body: &Body<'tcx>) -> bool {
164180
match callee.def {
165181
InstanceDef::Item(_) => {
166182
// If there is no MIR available (either because it was not in metadata or
167183
// because it has no MIR because it's an extern function), then the inliner
168184
// won't cause cycles on this.
169185
if !self.tcx.is_mir_available(callee.def_id()) {
170-
return false;
186+
return Err("item MIR unavailable");
171187
}
172188
}
173189
// These have no own callable MIR.
174-
InstanceDef::Intrinsic(_) | InstanceDef::Virtual(..) => return false,
190+
InstanceDef::Intrinsic(_) | InstanceDef::Virtual(..) => {
191+
return Err("instance without MIR (intrinsic / virtual)");
192+
}
175193
// This cannot result in an immediate cycle since the callee MIR is a shim, which does
176194
// not get any optimizations run on it. Any subsequent inlining may cause cycles, but we
177195
// do not need to catch this here, we can wait until the inliner decides to continue
@@ -181,13 +199,13 @@ impl Inliner<'tcx> {
181199
| InstanceDef::FnPtrShim(..)
182200
| InstanceDef::ClosureOnceShim { .. }
183201
| InstanceDef::DropGlue(..)
184-
| InstanceDef::CloneShim(..) => return true,
202+
| InstanceDef::CloneShim(..) => return Ok(()),
185203
}
186204

187205
if self.tcx.is_constructor(callee.def_id()) {
188206
trace!("constructors always have MIR");
189207
// Constructor functions cannot cause a query cycle.
190-
return true;
208+
return Ok(());
191209
}
192210

193211
if let Some(callee_def_id) = callee.def_id().as_local() {
@@ -196,39 +214,44 @@ impl Inliner<'tcx> {
196214
// since their `optimized_mir` is used for layout computation, which can
197215
// create a cycle, even when no attempt is made to inline the function
198216
// in the other direction.
199-
caller_body.generator_kind.is_none()
200-
&& (
201-
// Avoid a cycle here by only using `instance_mir` only if we have
202-
// a lower `HirId` than the callee. This ensures that the callee will
203-
// not inline us. This trick only works without incremental compilation.
204-
// So don't do it if that is enabled.
205-
!self.tcx.dep_graph.is_fully_enabled()
206-
&& self.hir_id < callee_hir_id
207-
// If we know for sure that the function we're calling will itself try to
208-
// call us, then we avoid inlining that function.
209-
|| !self.tcx.mir_callgraph_reachable((callee, caller_body.source.def_id().expect_local()))
210-
)
217+
if caller_body.generator_kind.is_some() {
218+
return Err("local generator (query cycle avoidance)");
219+
}
220+
221+
// Avoid a cycle here by only using `instance_mir` only if we have
222+
// a lower `HirId` than the callee. This ensures that the callee will
223+
// not inline us. This trick only works without incremental compilation.
224+
// So don't do it if that is enabled.
225+
if !self.tcx.dep_graph.is_fully_enabled() && self.hir_id < callee_hir_id {
226+
return Ok(());
227+
}
228+
229+
// If we know for sure that the function we're calling will itself try to
230+
// call us, then we avoid inlining that function.
231+
if self
232+
.tcx
233+
.mir_callgraph_reachable((*callee, caller_body.source.def_id().expect_local()))
234+
{
235+
return Err("caller might be reachable from callee (query cycle avoidance)");
236+
}
237+
238+
Ok(())
211239
} else {
212240
// This cannot result in an immediate cycle since the callee MIR is from another crate
213241
// and is already optimized. Any subsequent inlining may cause cycles, but we do
214242
// not need to catch this here, we can wait until the inliner decides to continue
215243
// inlining a second time.
216244
trace!("functions from other crates always have MIR");
217-
true
245+
Ok(())
218246
}
219247
}
220248

221-
fn get_valid_function_call(
249+
fn resolve_callsite(
222250
&self,
251+
caller_body: &Body<'tcx>,
223252
bb: BasicBlock,
224253
bb_data: &BasicBlockData<'tcx>,
225-
caller_body: &Body<'tcx>,
226254
) -> Option<CallSite<'tcx>> {
227-
// Don't inline calls that are in cleanup blocks.
228-
if bb_data.is_cleanup {
229-
return None;
230-
}
231-
232255
// Only consider direct calls to functions
233256
let terminator = bb_data.terminator();
234257
if let TerminatorKind::Call { ref func, ref destination, .. } = terminator.kind {
@@ -258,73 +281,73 @@ impl Inliner<'tcx> {
258281
None
259282
}
260283

261-
#[instrument(level = "debug", skip(self, callee_body))]
262-
fn should_inline(&self, callsite: CallSite<'tcx>, callee_body: &Body<'tcx>) -> bool {
263-
let tcx = self.tcx;
284+
/// Returns an error if inlining is not possible based on codegen attributes alone. A success
285+
/// indicates that inlining decision should be based on other criteria.
286+
fn check_codegen_attributes(
287+
&self,
288+
callsite: &CallSite<'tcx>,
289+
callee_attrs: &CodegenFnAttrs,
290+
) -> Result<(), &'satic str> {
291+
if let InlineAttr::Never = callee_attrs.inline {
292+
return Err("never inline hint");
293+
}
294+
295+
// Only inline local functions if they would be eligible for cross-crate
296+
// inlining. This is to ensure that the final crate doesn't have MIR that
297+
// reference unexported symbols
298+
if callsite.callee.def_id().is_local() {
299+
let is_generic = callsite.callee.substs.non_erasable_generics().next().is_some();
300+
if !is_generic && !callee_attrs.requests_inline() {
301+
return Err("not exported");
302+
}
303+
}
264304

265305
if callsite.fn_sig.c_variadic() {
266-
debug!("callee is variadic - not inlining");
267-
return false;
306+
return Err("C variadic");
268307
}
269308

270-
let codegen_fn_attrs = tcx.codegen_fn_attrs(callsite.callee.def_id());
309+
if callee_attrs.flags.contains(CodegenFnAttrFlags::NAKED) {
310+
return Err("naked");
311+
}
271312

272-
let self_features = &self.codegen_fn_attrs.target_features;
273-
let callee_features = &codegen_fn_attrs.target_features;
274-
if callee_features.iter().any(|feature| !self_features.contains(feature)) {
275-
debug!("`callee has extra target features - not inlining");
276-
return false;
313+
if callee_attrs.flags.contains(CodegenFnAttrFlags::COLD) {
314+
return Err("cold");
277315
}
278316

279-
if self.codegen_fn_attrs.no_sanitize != codegen_fn_attrs.no_sanitize {
280-
debug!("`callee has incompatible no_sanitize attribute - not inlining");
281-
return false;
317+
if callee_attrs.no_sanitize != self.codegen_fn_attrs.no_sanitize {
318+
return Err("incompatible sanitizer set");
282319
}
283320

284-
if self.codegen_fn_attrs.instruction_set != codegen_fn_attrs.instruction_set {
285-
debug!("`callee has incompatible instruction set - not inlining");
286-
return false;
321+
if callee_attrs.instruction_set != self.codegen_fn_attrs.instruction_set {
322+
return Err("incompatible instruction set");
287323
}
288324

289-
let hinted = match codegen_fn_attrs.inline {
290-
// Just treat inline(always) as a hint for now,
291-
// there are cases that prevent inlining that we
292-
// need to check for first.
293-
attr::InlineAttr::Always => true,
294-
attr::InlineAttr::Never => {
295-
debug!("`#[inline(never)]` present - not inlining");
296-
return false;
297-
}
298-
attr::InlineAttr::Hint => true,
299-
attr::InlineAttr::None => false,
300-
};
301-
302-
// Only inline local functions if they would be eligible for cross-crate
303-
// inlining. This is to ensure that the final crate doesn't have MIR that
304-
// reference unexported symbols
305-
if callsite.callee.def_id().is_local() {
306-
if callsite.callee.substs.non_erasable_generics().count() == 0 && !hinted {
307-
debug!(" callee is an exported function - not inlining");
308-
return false;
325+
for feature in &callee_attrs.target_features {
326+
if !self.codegen_fn_attrs.target_features.contains(feature) {
327+
return Err("incompatible target feature");
309328
}
310329
}
311330

312-
let mut threshold = if hinted {
331+
Ok(())
332+
}
333+
334+
/// Returns inlining decision that is based on the examination of callee MIR body.
335+
/// Assumes that codegen attributes have been checked for compatibility already.
336+
#[instrument(level = "debug", skip(self, callee_body))]
337+
fn check_mir_body(
338+
&self,
339+
callsite: &CallSite<'tcx>,
340+
callee_body: &Body<'tcx>,
341+
callee_attrs: &CodegenFnAttrs,
342+
) -> Result<(), &'static str> {
343+
let tcx = self.tcx;
344+
345+
let mut threshold = if callee_attrs.requests_inline() {
313346
self.tcx.sess.opts.debugging_opts.inline_mir_hint_threshold
314347
} else {
315348
self.tcx.sess.opts.debugging_opts.inline_mir_threshold
316349
};
317350

318-
if codegen_fn_attrs.flags.contains(CodegenFnAttrFlags::NAKED) {
319-
debug!("#[naked] present - not inlining");
320-
return false;
321-
}
322-
323-
if codegen_fn_attrs.flags.contains(CodegenFnAttrFlags::COLD) {
324-
debug!("#[cold] present - not inlining");
325-
return false;
326-
}
327-
328351
// Give a bonus functions with a small number of blocks,
329352
// We normally have two or three blocks for even
330353
// very small functions.
@@ -393,11 +416,10 @@ impl Inliner<'tcx> {
393416
if let Ok(Some(instance)) =
394417
Instance::resolve(self.tcx, self.param_env, def_id, substs)
395418
{
396-
if callsite.callee.def_id() == instance.def_id()
397-
|| self.history.contains(&instance)
398-
{
399-
debug!("`callee is recursive - not inlining");
400-
return false;
419+
if callsite.callee.def_id() == instance.def_id() {
420+
return Err("self-recursion");
421+
} else if self.history.contains(&instance) {
422+
return Err("already inlined");
401423
}
402424
}
403425
// Don't give intrinsics the extra penalty for calls
@@ -450,24 +472,24 @@ impl Inliner<'tcx> {
450472
}
451473
}
452474

453-
if let attr::InlineAttr::Always = codegen_fn_attrs.inline {
475+
if let InlineAttr::Always = callee_attrs.inline {
454476
debug!("INLINING {:?} because inline(always) [cost={}]", callsite, cost);
455-
true
477+
Ok(())
456478
} else {
457479
if cost <= threshold {
458480
debug!("INLINING {:?} [cost={} <= threshold={}]", callsite, cost, threshold);
459-
true
481+
Ok(())
460482
} else {
461483
debug!("NOT inlining {:?} [cost={} > threshold={}]", callsite, cost, threshold);
462-
false
484+
Err("cost above threshold")
463485
}
464486
}
465487
}
466488

467489
fn inline_call(
468490
&self,
469-
callsite: CallSite<'tcx>,
470491
caller_body: &mut Body<'tcx>,
492+
callsite: &CallSite<'tcx>,
471493
mut callee_body: Body<'tcx>,
472494
) {
473495
let terminator = caller_body[callsite.block].terminator.take().unwrap();

0 commit comments

Comments
 (0)