Create stable metric to measure long computation in Const Eval

This patch adds a `MirPass` that tracks the number of back-edges and
function calls in the CFG, adds a new MIR instruction to increment a
counter every time they are encountered during Const Eval, and emit a
warning if a configured limit is breached.
This commit is contained in:
Bryan Garza 2022-12-20 00:51:17 +00:00
parent c8e6a9e8b6
commit 360db516cc
37 changed files with 233 additions and 9 deletions

View File

@ -393,6 +393,7 @@ impl<'tcx> rustc_mir_dataflow::GenKillAnalysis<'tcx> for Borrows<'_, 'tcx> {
| mir::StatementKind::AscribeUserType(..)
| mir::StatementKind::Coverage(..)
| mir::StatementKind::Intrinsic(..)
| mir::StatementKind::ConstEvalCounter
| mir::StatementKind::Nop => {}
}
}

View File

@ -91,7 +91,8 @@ impl<'cx, 'tcx> Visitor<'tcx> for InvalidationGenerator<'cx, 'tcx> {
LocalMutationIsAllowed::Yes,
);
}
StatementKind::Nop
StatementKind::ConstEvalCounter
| StatementKind::Nop
| StatementKind::Retag { .. }
| StatementKind::Deinit(..)
| StatementKind::SetDiscriminant { .. } => {

View File

@ -620,7 +620,8 @@ impl<'cx, 'tcx> rustc_mir_dataflow::ResultsVisitor<'cx, 'tcx> for MirBorrowckCtx
flow_state,
);
}
StatementKind::Nop
StatementKind::ConstEvalCounter
| StatementKind::Nop
| StatementKind::Retag { .. }
| StatementKind::Deinit(..)
| StatementKind::SetDiscriminant { .. } => {

View File

@ -1258,6 +1258,7 @@ impl<'a, 'tcx> TypeChecker<'a, 'tcx> {
| StatementKind::StorageDead(..)
| StatementKind::Retag { .. }
| StatementKind::Coverage(..)
| StatementKind::ConstEvalCounter
| StatementKind::Nop => {}
StatementKind::Deinit(..) | StatementKind::SetDiscriminant { .. } => {
bug!("Statement not allowed in this MIR phase")

View File

@ -91,6 +91,7 @@ impl<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>> FunctionCx<'a, 'tcx, Bx> {
mir::StatementKind::FakeRead(..)
| mir::StatementKind::Retag { .. }
| mir::StatementKind::AscribeUserType(..)
| mir::StatementKind::ConstEvalCounter
| mir::StatementKind::Nop => {}
}
}

View File

@ -22,6 +22,8 @@ use crate::interpret::{
RefTracking, StackPopCleanup,
};
use tracing::info;
const NOTE_ON_UNDEFINED_BEHAVIOR_ERROR: &str = "The rules on what exactly is undefined behavior aren't clear, \
so this check might be overzealous. Please open an issue on the rustc \
repository if you believe it should not be considered undefined behavior.";
@ -33,6 +35,7 @@ fn eval_body_using_ecx<'mir, 'tcx>(
body: &'mir mir::Body<'tcx>,
) -> InterpResult<'tcx, MPlaceTy<'tcx>> {
debug!("eval_body_using_ecx: {:?}, {:?}", cid, ecx.param_env);
info!("HERE body is {:#?}", body);
let tcx = *ecx.tcx;
assert!(
cid.promoted.is_some()

View File

@ -369,6 +369,7 @@ impl<'mir, 'tcx> interpret::Machine<'mir, 'tcx> for CompileTimeInterpreter<'mir,
}
}
#[instrument(skip(ecx), ret)]
fn load_mir(
ecx: &InterpCx<'mir, 'tcx, Self>,
instance: ty::InstanceDef<'tcx>,

View File

@ -46,6 +46,9 @@ pub struct InterpCx<'mir, 'tcx, M: Machine<'mir, 'tcx>> {
/// The recursion limit (cached from `tcx.recursion_limit(())`)
pub recursion_limit: Limit,
pub const_eval_limit: u32,
pub const_eval_counter: u32,
}
// The Phantomdata exists to prevent this type from being `Send`. If it were sent across a thread
@ -408,6 +411,8 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
param_env,
memory: Memory::new(),
recursion_limit: tcx.recursion_limit(),
const_eval_limit: 20,
const_eval_counter: 0,
}
}

View File

@ -293,6 +293,17 @@ where
Prov: Provenance + 'static,
M: Machine<'mir, 'tcx, Provenance = Prov>,
{
pub fn increment_const_eval_counter(&mut self) {
self.const_eval_counter = self.const_eval_counter + 1;
if self.const_eval_counter == self.const_eval_limit {
let mut warn = self.tcx.sess.struct_warn(format!(
"Const eval counter limit ({}) has been crossed",
self.const_eval_limit
));
warn.emit();
}
}
/// Take a value, which represents a (thin or wide) reference, and make it a place.
/// Alignment is just based on the type. This is the inverse of `MemPlace::to_ref()`.
///

View File

@ -129,6 +129,11 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
// FIXME(#73156): Handle source code coverage in const eval
Coverage(..) => {}
// FIXME(bryangarza): Update this to do some logic!!!
ConstEvalCounter => {
self.increment_const_eval_counter();
}
// Defined to do nothing. These are added by optimization passes, to avoid changing the
// size of MIR constantly.
Nop => {}

View File

@ -693,6 +693,7 @@ impl<'tcx> Visitor<'tcx> for Checker<'_, 'tcx> {
| StatementKind::AscribeUserType(..)
| StatementKind::Coverage(..)
| StatementKind::Intrinsic(..)
| StatementKind::ConstEvalCounter
| StatementKind::Nop => {}
}
}

View File

@ -766,6 +766,7 @@ impl<'a, 'tcx> Visitor<'tcx> for TypeChecker<'a, 'tcx> {
StatementKind::StorageLive(..)
| StatementKind::StorageDead(..)
| StatementKind::Coverage(_)
| StatementKind::ConstEvalCounter
| StatementKind::Nop => {}
}

View File

@ -1461,6 +1461,7 @@ impl Debug for Statement<'_> {
}
Coverage(box ref coverage) => write!(fmt, "Coverage::{:?}", coverage.kind),
Intrinsic(box ref intrinsic) => write!(fmt, "{intrinsic}"),
ConstEvalCounter => write!(fmt, "ConstEvalCounter"),
Nop => write!(fmt, "nop"),
}
}

View File

@ -441,10 +441,14 @@ impl<'tcx> TyCtxt<'tcx> {
#[inline]
pub fn mir_for_ctfe_opt_const_arg(self, def: ty::WithOptConstParam<DefId>) -> &'tcx Body<'tcx> {
if let Some((did, param_did)) = def.as_const_arg() {
let res = if let Some((did, param_did)) = def.as_const_arg() {
info!("calling mir_for_ctfe_of_const_arg for DedId {did:?}");
self.mir_for_ctfe_of_const_arg((did, param_did))
} else {
info!("calling mir_for_ctfe for DefId {:?}", def.did);
self.mir_for_ctfe(def.did)
}
};
//info!("RES OF CALLING MIR_FOR_CTFE_OPT_CONST_ARG: {:#?}", res);
res
}
}

View File

@ -250,6 +250,7 @@ pub fn statement_kind_name(statement: &Statement<'_>) -> &'static str {
AscribeUserType(..) => "AscribeUserType",
Coverage(..) => "Coverage",
Intrinsic(..) => "Intrinsic",
ConstEvalCounter => "ConstEvalCounter",
Nop => "Nop",
}
}

View File

@ -286,7 +286,10 @@ pub enum StatementKind<'tcx> {
/// This is permitted for both generators and ADTs. This does not necessarily write to the
/// entire place; instead, it writes to the minimum set of bytes as required by the layout for
/// the type.
SetDiscriminant { place: Box<Place<'tcx>>, variant_index: VariantIdx },
SetDiscriminant {
place: Box<Place<'tcx>>,
variant_index: VariantIdx,
},
/// Deinitializes the place.
///
@ -355,6 +358,8 @@ pub enum StatementKind<'tcx> {
/// This avoids adding a new block and a terminator for simple intrinsics.
Intrinsic(Box<NonDivergingIntrinsic<'tcx>>),
ConstEvalCounter,
/// No-op. Useful for deleting instructions without affecting statement indices.
Nop,
}

View File

@ -427,6 +427,7 @@ macro_rules! make_mir_visitor {
}
}
}
StatementKind::ConstEvalCounter => {}
StatementKind::Nop => {}
}
}

View File

@ -271,6 +271,7 @@ impl<'a, 'tcx> Analysis<'tcx> for MaybeTransitiveLiveLocals<'a> {
| StatementKind::AscribeUserType(..)
| StatementKind::Coverage(..)
| StatementKind::Intrinsic(..)
| StatementKind::ConstEvalCounter
| StatementKind::Nop => None,
};
if let Some(destination) = destination {

View File

@ -141,6 +141,7 @@ impl<'mir, 'tcx> crate::GenKillAnalysis<'tcx> for MaybeRequiresStorage<'mir, 'tc
StatementKind::AscribeUserType(..)
| StatementKind::Coverage(..)
| StatementKind::FakeRead(..)
| StatementKind::ConstEvalCounter
| StatementKind::Nop
| StatementKind::Retag(..)
| StatementKind::Intrinsic(..)

View File

@ -331,6 +331,7 @@ impl<'b, 'a, 'tcx> Gatherer<'b, 'a, 'tcx> {
| StatementKind::AscribeUserType(..)
| StatementKind::Coverage(..)
| StatementKind::Intrinsic(..)
| StatementKind::ConstEvalCounter
| StatementKind::Nop => {}
}
}

View File

@ -84,7 +84,8 @@ pub trait ValueAnalysis<'tcx> {
StatementKind::Retag(..) => {
// We don't track references.
}
StatementKind::Nop
StatementKind::ConstEvalCounter
| StatementKind::Nop
| StatementKind::FakeRead(..)
| StatementKind::Coverage(..)
| StatementKind::AscribeUserType(..) => (),

View File

@ -104,6 +104,7 @@ impl<'tcx> Visitor<'tcx> for UnsafetyChecker<'_, 'tcx> {
| StatementKind::AscribeUserType(..)
| StatementKind::Coverage(..)
| StatementKind::Intrinsic(..)
| StatementKind::ConstEvalCounter
| StatementKind::Nop => {
// safe (at least as emitted during MIR construction)
}

View File

@ -802,6 +802,8 @@ pub(super) fn filtered_statement_span(statement: &Statement<'_>) -> Option<Span>
| StatementKind::StorageDead(_)
// Coverage should not be encountered, but don't inject coverage coverage
| StatementKind::Coverage(_)
// Ignore `ConstEvalCounter`s
| StatementKind::ConstEvalCounter
// Ignore `Nop`s
| StatementKind::Nop => None,

View File

@ -0,0 +1,92 @@
use crate::MirPass;
use rustc_middle::mir::{BasicBlock, Body, Statement, StatementKind, TerminatorKind};
use rustc_middle::ty::TyCtxt;
use tracing::{info, instrument};
pub struct CtfeLimit;
impl<'tcx> MirPass<'tcx> for CtfeLimit {
#[instrument(skip(self, _tcx, body))]
fn run_pass(&self, _tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
let doms = body.basic_blocks.dominators();
//info!("Got body with {} basic blocks: {:#?}", body.basic_blocks.len(), body.basic_blocks);
//info!("With doms: {doms:?}");
/*
for (index, basic_block) in body.basic_blocks.iter().enumerate() {
info!("bb{index}: {basic_block:#?}")
}*/
for (index, basic_block) in body.basic_blocks.iter().enumerate() {
info!(
"bb{index} -> successors = {:?}",
basic_block.terminator().successors().collect::<Vec<BasicBlock>>()
);
}
for (index, basic_block) in body.basic_blocks.iter().enumerate() {
info!("bb{index} -> unwind = {:?}", basic_block.terminator().unwind())
}
let mut dominators = Vec::new();
for idom in 0..body.basic_blocks.len() {
let mut nodes = Vec::new();
for inode in 0..body.basic_blocks.len() {
let dom = BasicBlock::from_usize(idom);
let node = BasicBlock::from_usize(inode);
if doms.is_reachable(dom)
&& doms.is_reachable(node)
&& doms.is_dominated_by(node, dom)
{
//info!("{idom} dominates {inode}");
nodes.push(true);
} else {
nodes.push(false);
}
}
dominators.push(nodes);
}
/*
for idom in 0..body.basic_blocks.len() {
print!("{idom} | dom | ");
for inode in 0..body.basic_blocks.len() {
if dominators[idom][inode] {
print!("{inode} | ");
} else {
print!(" | ");
}
}
print!("\n");
}
*/
for (index, basic_block) in body.basic_blocks_mut().iter_mut().enumerate() {
// info!("bb{index}: {basic_block:#?}");
//info!("bb{index} -> successors = {:?}", basic_block.terminator().successors().collect::<Vec<BasicBlock>>());
let is_back_edge_or_fn_call = 'label: {
match basic_block.terminator().kind {
TerminatorKind::Call { .. } => {
break 'label true;
}
_ => (),
}
for successor in basic_block.terminator().successors() {
let s_index = successor.as_usize();
if dominators[s_index][index] {
info!("{s_index} to {index} is a loop");
break 'label true;
}
}
false
};
if is_back_edge_or_fn_call {
basic_block.statements.push(Statement {
source_info: basic_block.terminator().source_info,
kind: StatementKind::ConstEvalCounter,
});
info!("New basic block statements vector: {:?}", basic_block.statements);
}
}
info!("With doms: {doms:?}");
}
}

View File

@ -53,6 +53,7 @@ pub fn eliminate<'tcx>(tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>, borrowed: &BitS
| StatementKind::StorageDead(_)
| StatementKind::Coverage(_)
| StatementKind::Intrinsic(_)
| StatementKind::ConstEvalCounter
| StatementKind::Nop => (),
StatementKind::FakeRead(_) | StatementKind::AscribeUserType(_, _) => {

View File

@ -577,6 +577,7 @@ impl WriteInfo {
self.add_place(**place);
}
StatementKind::Intrinsic(_)
| StatementKind::ConstEvalCounter
| StatementKind::Nop
| StatementKind::Coverage(_)
| StatementKind::StorageLive(_)

View File

@ -1583,6 +1583,7 @@ impl<'tcx> Visitor<'tcx> for EnsureGeneratorFieldAssignmentsNeverAlias<'_> {
| StatementKind::AscribeUserType(..)
| StatementKind::Coverage(..)
| StatementKind::Intrinsic(..)
| StatementKind::ConstEvalCounter
| StatementKind::Nop => {}
}
}

View File

@ -55,6 +55,7 @@ mod const_goto;
mod const_prop;
mod const_prop_lint;
mod coverage;
mod ctfe_limit;
mod dataflow_const_prop;
mod dead_store_elimination;
mod deaggregator;
@ -349,11 +350,14 @@ fn mir_promoted(
/// Compute the MIR that is used during CTFE (and thus has no optimizations run on it)
fn mir_for_ctfe(tcx: TyCtxt<'_>, def_id: DefId) -> &Body<'_> {
let did = def_id.expect_local();
if let Some(def) = ty::WithOptConstParam::try_lookup(did, tcx) {
let body = if let Some(def) = ty::WithOptConstParam::try_lookup(did, tcx) {
tcx.mir_for_ctfe_of_const_arg(def)
} else {
tcx.arena.alloc(inner_mir_for_ctfe(tcx, ty::WithOptConstParam::unknown(did)))
}
};
//info!("MIR_FOR_CTFE (DefId = {def_id:?}) body res: {:#?}", body);
info!("MIR_FOR_CTFE (DefId = {def_id:?})");
body
}
/// Same as `mir_for_ctfe`, but used to get the MIR of a const generic parameter.
@ -447,6 +451,7 @@ fn mir_drops_elaborated_and_const_checked(
run_analysis_to_runtime_passes(tcx, &mut body);
//info!("MIR after runtime passes: {:#?}", body);
tcx.alloc_steal_mir(body)
}
@ -517,6 +522,7 @@ fn run_runtime_lowering_passes<'tcx>(tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
// CTFE support for aggregates.
&deaggregator::Deaggregator,
&Lint(const_prop_lint::ConstProp),
&ctfe_limit::CtfeLimit,
];
pm::run_passes_no_validate(tcx, body, passes, Some(MirPhase::Runtime(RuntimePhase::Initial)));
}
@ -617,6 +623,7 @@ fn inner_optimized_mir(tcx: TyCtxt<'_>, did: LocalDefId) -> Body<'_> {
let mut body = remap_mir_for_const_eval_select(tcx, body, hir::Constness::NotConst);
debug!("body: {:#?}", body);
run_optimization_passes(tcx, &mut body);
//info!("body after OPTIMIZATION: {:#?}", body);
debug_assert!(!body.has_free_regions(), "Free regions in optimized MIR");

View File

@ -35,6 +35,7 @@ impl RemoveNoopLandingPads {
| StatementKind::StorageDead(_)
| StatementKind::AscribeUserType(..)
| StatementKind::Coverage(..)
| StatementKind::ConstEvalCounter
| StatementKind::Nop => {
// These are all noops in a landing pad
}

View File

@ -250,6 +250,7 @@ fn is_likely_const<'tcx>(mut tracked_place: Place<'tcx>, block: &BasicBlockData<
| StatementKind::Coverage(_)
| StatementKind::StorageDead(_)
| StatementKind::Intrinsic(_)
| StatementKind::ConstEvalCounter
| StatementKind::Nop => {}
}
}
@ -318,6 +319,7 @@ fn find_determining_place<'tcx>(
| StatementKind::AscribeUserType(_, _)
| StatementKind::Coverage(_)
| StatementKind::Intrinsic(_)
| StatementKind::ConstEvalCounter
| StatementKind::Nop => {}
// If the discriminant is set, it is always set

View File

@ -517,7 +517,7 @@ impl<'tcx> Visitor<'tcx> for UsedLocals {
self.super_statement(statement, location);
}
StatementKind::Nop => {}
StatementKind::ConstEvalCounter | StatementKind::Nop => {}
StatementKind::StorageLive(_local) | StatementKind::StorageDead(_local) => {}

View File

@ -0,0 +1,25 @@
// check-pass
#![feature(const_for)]
const fn labelled_loop() -> u32 {
let mut n = 0;
'outer: loop {
'inner: loop {
n = n + 1;
if n > 5 && n <= 10 {
n = n + 1;
continue 'inner
}
if n > 30 {
break 'outer
}
}
}
n
}
const X: u32 = labelled_loop();
fn main() {
println!("{X}");
}

View File

@ -0,0 +1,4 @@
warning: Const eval counter limit (20) has been crossed
warning: 1 warning emitted

View File

@ -0,0 +1,15 @@
// check-pass
const fn recurse(n: u32) -> u32 {
if n == 0 {
n
} else {
recurse(n - 1)
}
}
const X: u32 = recurse(30);
fn main() {
println!("{X}");
}

View File

@ -0,0 +1,4 @@
warning: Const eval counter limit (20) has been crossed
warning: 1 warning emitted

View File

@ -0,0 +1,16 @@
// check-pass
const fn simple_loop(n: u32) -> u32 {
let mut index = 0;
let mut res = 0;
while index < n {
res = res + index;
index = index + 1;
}
res
}
const X: u32 = simple_loop(30);
fn main() {
println!("{X}");
}

View File

@ -0,0 +1,4 @@
warning: Const eval counter limit (20) has been crossed
warning: 1 warning emitted