Skip to content

Commit 34b5166

Browse files
committed
Auto merge of rust-lang#121421 - saethlin:smarter-mono, r=<try>
Avoid lowering code under dead SwitchInt targets The objective of this PR is to detect and eliminate code which is guarded by an `if false`, even if that `false` is a constant which is not known until monomorphization, or is `intrinsics::debug_assertions()`. The effect of this is that we generate no LLVM IR the standard library's unsafe preconditions, when they are compiled in a build where they should be immediately optimized out. This mono-time optimization ensures that builds which disable debug assertions do not grow a linkage requirement against `core`, which compiler-builtins currently needs: rust-lang#121552 This revives the codegen side of rust-lang#91222 as planned in rust-lang#120848.
2 parents 89d8e31 + 8885de2 commit 34b5166

File tree

6 files changed

+211
-4
lines changed

6 files changed

+211
-4
lines changed

compiler/rustc_codegen_ssa/src/mir/block.rs

+10
Original file line numberDiff line numberDiff line change
@@ -1186,6 +1186,16 @@ impl<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>> FunctionCx<'a, 'tcx, Bx> {
11861186
}
11871187
}
11881188

1189+
pub fn codegen_block_as_unreachable(&mut self, bb: mir::BasicBlock) {
1190+
let llbb = match self.try_llbb(bb) {
1191+
Some(llbb) => llbb,
1192+
None => return,
1193+
};
1194+
let bx = &mut Bx::build(self.cx, llbb);
1195+
debug!("codegen_block_as_unreachable({:?})", bb);
1196+
bx.unreachable();
1197+
}
1198+
11891199
fn codegen_terminator(
11901200
&mut self,
11911201
bx: &mut Bx,

compiler/rustc_codegen_ssa/src/mir/mod.rs

+10-1
Original file line numberDiff line numberDiff line change
@@ -256,13 +256,22 @@ pub fn codegen_mir<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>>(
256256
// Apply debuginfo to the newly allocated locals.
257257
fx.debug_introduce_locals(&mut start_bx);
258258

259+
let reachable_blocks = mir.reachable_blocks_in_mono(cx.tcx(), instance);
260+
259261
// The builders will be created separately for each basic block at `codegen_block`.
260262
// So drop the builder of `start_llbb` to avoid having two at the same time.
261263
drop(start_bx);
262264

263265
// Codegen the body of each block using reverse postorder
264266
for (bb, _) in traversal::reverse_postorder(mir) {
265-
fx.codegen_block(bb);
267+
if reachable_blocks.contains(bb) {
268+
fx.codegen_block(bb);
269+
} else {
270+
// This may have references to things we didn't monomorphize, so we
271+
// don't actually codegen the body. We still create the block so
272+
// terminators in other blocks can reference it without worry.
273+
fx.codegen_block_as_unreachable(bb);
274+
}
266275
}
267276
}
268277

compiler/rustc_middle/src/mir/mod.rs

+126-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use crate::ty::print::{pretty_print_const, with_no_trimmed_paths};
1010
use crate::ty::print::{FmtPrinter, Printer};
1111
use crate::ty::visit::TypeVisitableExt;
1212
use crate::ty::{self, List, Ty, TyCtxt};
13-
use crate::ty::{AdtDef, InstanceDef, UserTypeAnnotationIndex};
13+
use crate::ty::{AdtDef, Instance, InstanceDef, UserTypeAnnotationIndex};
1414
use crate::ty::{GenericArg, GenericArgsRef};
1515

1616
use rustc_data_structures::captures::Captures;
@@ -29,6 +29,7 @@ pub use rustc_ast::Mutability;
2929
use rustc_data_structures::fx::FxHashMap;
3030
use rustc_data_structures::fx::FxHashSet;
3131
use rustc_data_structures::graph::dominators::Dominators;
32+
use rustc_index::bit_set::BitSet;
3233
use rustc_index::{Idx, IndexSlice, IndexVec};
3334
use rustc_serialize::{Decodable, Encodable};
3435
use rustc_span::symbol::Symbol;
@@ -642,6 +643,130 @@ impl<'tcx> Body<'tcx> {
642643
self.injection_phase.is_some()
643644
}
644645

646+
/// Finds which basic blocks are actually reachable for a specific
647+
/// monomorphization of this body.
648+
///
649+
/// This is allowed to have false positives; just because this says a block
650+
/// is reachable doesn't mean that's necessarily true. It's thus always
651+
/// legal for this to return a filled set.
652+
///
653+
/// Regardless, the [`BitSet::domain_size`] of the returned set will always
654+
/// exactly match the number of blocks in the body so that `contains`
655+
/// checks can be done without worrying about panicking.
656+
///
657+
/// This is mostly useful because it lets us skip lowering the `false` side
658+
/// of `if <T as Trait>::CONST`, as well as `intrinsics::debug_assertions`.
659+
pub fn reachable_blocks_in_mono(
660+
&self,
661+
tcx: TyCtxt<'tcx>,
662+
instance: Instance<'tcx>,
663+
) -> BitSet<BasicBlock> {
664+
let mut set = BitSet::new_empty(self.basic_blocks.len());
665+
self.reachable_blocks_in_mono_from(tcx, instance, &mut set, START_BLOCK);
666+
set
667+
}
668+
669+
fn reachable_blocks_in_mono_from(
670+
&self,
671+
tcx: TyCtxt<'tcx>,
672+
instance: Instance<'tcx>,
673+
set: &mut BitSet<BasicBlock>,
674+
bb: BasicBlock,
675+
) {
676+
if !set.insert(bb) {
677+
return;
678+
}
679+
680+
let data = &self.basic_blocks[bb];
681+
682+
if let Some((bits, targets)) = Self::try_const_mono_switchint(tcx, instance, data) {
683+
let target = targets.target_for_value(bits);
684+
return self.reachable_blocks_in_mono_from(tcx, instance, set, target);
685+
}
686+
687+
for target in data.terminator().successors() {
688+
self.reachable_blocks_in_mono_from(tcx, instance, set, target);
689+
}
690+
}
691+
692+
/// If this basic block ends with a [`TerminatorKind::SwitchInt`] for which we can evaluate the
693+
/// dimscriminant in monomorphization, we return the discriminant bits and the
694+
/// [`SwitchTargets`], just so the caller doesn't also have to match on the terminator.
695+
fn try_const_mono_switchint<'a>(
696+
tcx: TyCtxt<'tcx>,
697+
instance: Instance<'tcx>,
698+
block: &'a BasicBlockData<'tcx>,
699+
) -> Option<(u128, &'a SwitchTargets)> {
700+
// There are two places here we need to evaluate a constant.
701+
let eval_mono_const = |constant: &ConstOperand<'tcx>| {
702+
let env = ty::ParamEnv::reveal_all();
703+
let mono_literal = instance.instantiate_mir_and_normalize_erasing_regions(
704+
tcx,
705+
env,
706+
crate::ty::EarlyBinder::bind(constant.const_),
707+
);
708+
let Some(bits) = mono_literal.try_eval_bits(tcx, env) else {
709+
bug!("Couldn't evaluate constant {:?} in mono {:?}", constant, instance);
710+
};
711+
bits
712+
};
713+
714+
let TerminatorKind::SwitchInt { discr, targets } = &block.terminator().kind else {
715+
return None;
716+
};
717+
718+
// If this is a SwitchInt(const _), then we can just evaluate the constant and return.
719+
let discr = match discr {
720+
Operand::Constant(constant) => {
721+
let bits = eval_mono_const(constant);
722+
return Some((bits, targets));
723+
}
724+
Operand::Move(place) | Operand::Copy(place) => place,
725+
};
726+
727+
// MIR for `if false` actually looks like this:
728+
// _1 = const _
729+
// SwitchInt(_1)
730+
//
731+
// And MIR for if intrinsics::debug_assertions() looks like this:
732+
// _1 = cfg!(debug_assertions)
733+
// SwitchInt(_1)
734+
//
735+
// So we're going to try to recognize this pattern.
736+
//
737+
// If we have a SwitchInt on a non-const place, we find the most recent statement that
738+
// isn't a storage marker. If that statement is an assignment of a const to our
739+
// discriminant place, we evaluate and return the const, as if we've const-propagated it
740+
// into the SwitchInt.
741+
742+
let last_stmt = block
743+
.statements
744+
.iter()
745+
.rev()
746+
.skip_while(|stmt| {
747+
matches!(stmt.kind, StatementKind::StorageDead(_) | StatementKind::StorageLive(_))
748+
})
749+
.next()?;
750+
let StatementKind::Assign(box (place, rvalue)) = &last_stmt.kind else {
751+
return None;
752+
};
753+
754+
if discr != place {
755+
return None;
756+
}
757+
758+
match rvalue {
759+
Rvalue::NullaryOp(NullOp::DebugAssertions, _) => {
760+
Some((tcx.sess.opts.debug_assertions as u128, targets))
761+
}
762+
Rvalue::Use(Operand::Constant(constant)) => {
763+
let bits = eval_mono_const(constant);
764+
Some((bits, targets))
765+
}
766+
_ => None,
767+
}
768+
}
769+
645770
/// For a `Location` in this scope, determine what the "caller location" at that point is. This
646771
/// is interesting because of inlining: the `#[track_caller]` attribute of inlined functions
647772
/// must be honored. Falls back to the `tracked_caller` value for `#[track_caller]` functions,

compiler/rustc_middle/src/mir/traversal.rs

-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
use rustc_index::bit_set::BitSet;
2-
31
use super::*;
42

53
/// Preorder traversal of a graph.

tests/codegen/precondition-checks.rs

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//@ compile-flags: -Cno-prepopulate-passes -Copt-level=0 -Cdebug-assertions=no
2+
3+
// This test ensures that in a debug build which turns off debug assertions, we do not monomorphize
4+
// any of the standard library's unsafe precondition checks.
5+
// The naive codegen of those checks contains the actual check underneath an `if false`, which
6+
// could be optimized out if optimizations are enabled. But if we rely on optimizations to remove
7+
// panic branches, then we can't link compiler_builtins without optimizing it, which means that
8+
// -Zbuild-std doesn't work with -Copt-level=0.
9+
//
10+
// In other words, this tests for a mandatory optimization.
11+
12+
#![crate_type = "lib"]
13+
14+
use std::ptr::NonNull;
15+
16+
// CHECK-LABEL: ; core::ptr::non_null::NonNull<T>::new_unchecked
17+
// CHECK-NOT: call
18+
19+
// CHECK-LABEL: @nonnull_new
20+
#[no_mangle]
21+
pub unsafe fn nonnull_new(ptr: *mut u8) -> NonNull<u8> {
22+
// CHECK: ; call core::ptr::non_null::NonNull<T>::new_unchecked
23+
unsafe {
24+
NonNull::new_unchecked(ptr)
25+
}
26+
}
+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//@ compile-flags: -Cno-prepopulate-passes -Copt-level=0
2+
3+
#![crate_type = "lib"]
4+
5+
#[no_mangle]
6+
pub fn demo_for_i32() {
7+
generic_impl::<i32>();
8+
}
9+
10+
// Two important things here:
11+
// - We replace the "then" block with `unreachable` to avoid linking problems
12+
// - We neither declare nor define the `big_impl` that said block "calls".
13+
14+
// CHECK-LABEL: ; skip_mono_inside_if_false::generic_impl
15+
// CHECK: start:
16+
// CHECK-NEXT: br i1 false, label %[[THEN_BRANCH:bb[0-9]+]], label %[[ELSE_BRANCH:bb[0-9]+]]
17+
// CHECK: [[ELSE_BRANCH]]:
18+
// CHECK-NEXT: call skip_mono_inside_if_false::small_impl
19+
// CHECK: [[THEN_BRANCH]]:
20+
// CHECK-NEXT: unreachable
21+
22+
fn generic_impl<T>() {
23+
trait MagicTrait {
24+
const IS_BIG: bool;
25+
}
26+
impl<T> MagicTrait for T {
27+
const IS_BIG: bool = std::mem::size_of::<T>() > 10;
28+
}
29+
if T::IS_BIG {
30+
big_impl::<T>();
31+
} else {
32+
small_impl::<T>();
33+
}
34+
}
35+
36+
#[inline(never)]
37+
fn small_impl<T>() {}
38+
#[inline(never)]
39+
fn big_impl<T>() {}

0 commit comments

Comments
 (0)