Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 5a6c1aa

Browse files
committedMar 13, 2024
Auto merge of rust-lang#121421 - saethlin:smarter-mono, r=oli-obk
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 d3555f3 + 81d6304 commit 5a6c1aa

File tree

6 files changed

+214
-4
lines changed

6 files changed

+214
-4
lines changed
 

‎compiler/rustc_codegen_ssa/src/mir/block.rs

+10
Original file line numberDiff line numberDiff line change
@@ -1237,6 +1237,16 @@ impl<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>> FunctionCx<'a, 'tcx, Bx> {
12371237
}
12381238
}
12391239

1240+
pub fn codegen_block_as_unreachable(&mut self, bb: mir::BasicBlock) {
1241+
let llbb = match self.try_llbb(bb) {
1242+
Some(llbb) => llbb,
1243+
None => return,
1244+
};
1245+
let bx = &mut Bx::build(self.cx, llbb);
1246+
debug!("codegen_block_as_unreachable({:?})", bb);
1247+
bx.unreachable();
1248+
}
1249+
12401250
fn codegen_terminator(
12411251
&mut self,
12421252
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;
@@ -27,6 +27,8 @@ pub use rustc_ast::Mutability;
2727
use rustc_data_structures::fx::FxHashMap;
2828
use rustc_data_structures::fx::FxHashSet;
2929
use rustc_data_structures::graph::dominators::Dominators;
30+
use rustc_data_structures::stack::ensure_sufficient_stack;
31+
use rustc_index::bit_set::BitSet;
3032
use rustc_index::{Idx, IndexSlice, IndexVec};
3133
use rustc_serialize::{Decodable, Encodable};
3234
use rustc_span::symbol::Symbol;
@@ -640,6 +642,129 @@ impl<'tcx> Body<'tcx> {
640642
self.injection_phase.is_some()
641643
}
642644

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

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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+
// CHECK: }
19+
20+
// CHECK-LABEL: @nonnull_new
21+
#[no_mangle]
22+
pub unsafe fn nonnull_new(ptr: *mut u8) -> NonNull<u8> {
23+
// CHECK: ; call core::ptr::non_null::NonNull<T>::new_unchecked
24+
unsafe {
25+
NonNull::new_unchecked(ptr)
26+
}
27+
}
+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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 label %[[ELSE_BRANCH:bb[0-9]+]]
17+
// CHECK: [[ELSE_BRANCH]]:
18+
// CHECK-NEXT: call skip_mono_inside_if_false::small_impl
19+
// CHECK: bb{{[0-9]+}}:
20+
// CHECK-NEXT: ret void
21+
// CHECK: bb{{[0-9+]}}:
22+
// CHECK-NEXT: unreachable
23+
24+
fn generic_impl<T>() {
25+
trait MagicTrait {
26+
const IS_BIG: bool;
27+
}
28+
impl<T> MagicTrait for T {
29+
const IS_BIG: bool = std::mem::size_of::<T>() > 10;
30+
}
31+
if T::IS_BIG {
32+
big_impl::<T>();
33+
} else {
34+
small_impl::<T>();
35+
}
36+
}
37+
38+
#[inline(never)]
39+
fn small_impl<T>() {}
40+
#[inline(never)]
41+
fn big_impl<T>() {}

0 commit comments

Comments
 (0)
Failed to load comments.