Skip to content

Commit cd6d8f2

Browse files
committed
Auto merge of rust-lang#118336 - saethlin:const-to-op-cache, r=RalfJung
Return a finite number of AllocIds per ConstAllocation in Miri Before this, every evaluation of a const slice would produce a new AllocId. So in Miri, this program used to have unbounded memory use: ```rust fn main() { loop { helper(); } } fn helper() { "ouch"; } ``` Every trip around the loop creates a new AllocId which we need to keep track of a base address for. And the provenance GC can never clean up that AllocId -> u64 mapping, because the AllocId is for a const allocation which will never be deallocated. So this PR moves the logic of producing an AllocId for a ConstAllocation to the Machine trait, and the implementation that Miri provides will only produce 16 AllocIds for each allocation. The cache is also keyed on the Instance that the const is evaluated in, so that equal consts evaluated in two functions will have disjoint base addresses. r? RalfJung
2 parents f6ee4bf + c8a675d commit cd6d8f2

File tree

4 files changed

+119
-9
lines changed

4 files changed

+119
-9
lines changed

compiler/rustc_const_eval/src/interpret/eval_context.rs

+9-7
Original file line numberDiff line numberDiff line change
@@ -1131,13 +1131,15 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
11311131
span: Option<Span>,
11321132
layout: Option<TyAndLayout<'tcx>>,
11331133
) -> InterpResult<'tcx, OpTy<'tcx, M::Provenance>> {
1134-
let const_val = val.eval(*self.tcx, self.param_env, span).map_err(|err| {
1135-
// FIXME: somehow this is reachable even when POST_MONO_CHECKS is on.
1136-
// Are we not always populating `required_consts`?
1137-
err.emit_note(*self.tcx);
1138-
err
1139-
})?;
1140-
self.const_val_to_op(const_val, val.ty(), layout)
1134+
M::eval_mir_constant(self, *val, span, layout, |ecx, val, span, layout| {
1135+
let const_val = val.eval(*ecx.tcx, ecx.param_env, span).map_err(|err| {
1136+
// FIXME: somehow this is reachable even when POST_MONO_CHECKS is on.
1137+
// Are we not always populating `required_consts`?
1138+
err.emit_note(*ecx.tcx);
1139+
err
1140+
})?;
1141+
ecx.const_val_to_op(const_val, val.ty(), layout)
1142+
})
11411143
}
11421144

11431145
#[must_use]

compiler/rustc_const_eval/src/interpret/machine.rs

+22
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use rustc_middle::query::TyCtxtAt;
1313
use rustc_middle::ty;
1414
use rustc_middle::ty::layout::TyAndLayout;
1515
use rustc_span::def_id::DefId;
16+
use rustc_span::Span;
1617
use rustc_target::abi::{Align, Size};
1718
use rustc_target::spec::abi::Abi as CallAbi;
1819

@@ -510,6 +511,27 @@ pub trait Machine<'mir, 'tcx: 'mir>: Sized {
510511
) -> InterpResult<'tcx> {
511512
Ok(())
512513
}
514+
515+
/// Evaluate the given constant. The `eval` function will do all the required evaluation,
516+
/// but this hook has the chance to do some pre/postprocessing.
517+
#[inline(always)]
518+
fn eval_mir_constant<F>(
519+
ecx: &InterpCx<'mir, 'tcx, Self>,
520+
val: mir::Const<'tcx>,
521+
span: Option<Span>,
522+
layout: Option<TyAndLayout<'tcx>>,
523+
eval: F,
524+
) -> InterpResult<'tcx, OpTy<'tcx, Self::Provenance>>
525+
where
526+
F: Fn(
527+
&InterpCx<'mir, 'tcx, Self>,
528+
mir::Const<'tcx>,
529+
Option<Span>,
530+
Option<TyAndLayout<'tcx>>,
531+
) -> InterpResult<'tcx, OpTy<'tcx, Self::Provenance>>,
532+
{
533+
eval(ecx, val, span, layout)
534+
}
513535
}
514536

515537
/// A lot of the flexibility above is just needed for `Miri`, but all "compile-time" machines

src/tools/miri/src/machine.rs

+50-2
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
44
use std::borrow::Cow;
55
use std::cell::{Cell, RefCell};
6+
use std::collections::hash_map::Entry;
67
use std::fmt;
78
use std::path::Path;
89
use std::process;
910

1011
use either::Either;
1112
use rand::rngs::StdRng;
1213
use rand::SeedableRng;
14+
use rand::Rng;
1315

1416
use rustc_ast::ast::Mutability;
1517
use rustc_data_structures::fx::{FxHashMap, FxHashSet};
@@ -45,6 +47,11 @@ pub const SIGRTMIN: i32 = 34;
4547
/// `SIGRTMAX` - `SIGRTMIN` >= 8 (which is the value of `_POSIX_RTSIG_MAX`)
4648
pub const SIGRTMAX: i32 = 42;
4749

50+
/// Each const has multiple addresses, but only this many. Since const allocations are never
51+
/// deallocated, choosing a new [`AllocId`] and thus base address for each evaluation would
52+
/// produce unbounded memory usage.
53+
const ADDRS_PER_CONST: usize = 16;
54+
4855
/// Extra data stored with each stack frame
4956
pub struct FrameExtra<'tcx> {
5057
/// Extra data for the Borrow Tracker.
@@ -65,12 +72,18 @@ pub struct FrameExtra<'tcx> {
6572
/// optimization.
6673
/// This is used by `MiriMachine::current_span` and `MiriMachine::caller_span`
6774
pub is_user_relevant: bool,
75+
76+
/// We have a cache for the mapping from [`mir::Const`] to resulting [`AllocId`].
77+
/// However, we don't want all frames to always get the same result, so we insert
78+
/// an additional bit of "salt" into the cache key. This salt is fixed per-frame
79+
/// so that within a call, a const will have a stable address.
80+
salt: usize,
6881
}
6982

7083
impl<'tcx> std::fmt::Debug for FrameExtra<'tcx> {
7184
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
7285
// Omitting `timing`, it does not support `Debug`.
73-
let FrameExtra { borrow_tracker, catch_unwind, timing: _, is_user_relevant: _ } = self;
86+
let FrameExtra { borrow_tracker, catch_unwind, timing: _, is_user_relevant: _, salt: _ } = self;
7487
f.debug_struct("FrameData")
7588
.field("borrow_tracker", borrow_tracker)
7689
.field("catch_unwind", catch_unwind)
@@ -80,7 +93,7 @@ impl<'tcx> std::fmt::Debug for FrameExtra<'tcx> {
8093

8194
impl VisitProvenance for FrameExtra<'_> {
8295
fn visit_provenance(&self, visit: &mut VisitWith<'_>) {
83-
let FrameExtra { catch_unwind, borrow_tracker, timing: _, is_user_relevant: _ } = self;
96+
let FrameExtra { catch_unwind, borrow_tracker, timing: _, is_user_relevant: _, salt: _ } = self;
8497

8598
catch_unwind.visit_provenance(visit);
8699
borrow_tracker.visit_provenance(visit);
@@ -552,6 +565,11 @@ pub struct MiriMachine<'mir, 'tcx> {
552565
/// The spans we will use to report where an allocation was created and deallocated in
553566
/// diagnostics.
554567
pub(crate) allocation_spans: RefCell<FxHashMap<AllocId, (Span, Option<Span>)>>,
568+
569+
/// Maps MIR consts to their evaluated result. We combine the const with a "salt" (`usize`)
570+
/// that is fixed per stack frame; this lets us have sometimes different results for the
571+
/// same const while ensuring consistent results within a single call.
572+
const_cache: RefCell<FxHashMap<(mir::Const<'tcx>, usize), OpTy<'tcx, Provenance>>>,
555573
}
556574

557575
impl<'mir, 'tcx> MiriMachine<'mir, 'tcx> {
@@ -677,6 +695,7 @@ impl<'mir, 'tcx> MiriMachine<'mir, 'tcx> {
677695
stack_size,
678696
collect_leak_backtraces: config.collect_leak_backtraces,
679697
allocation_spans: RefCell::new(FxHashMap::default()),
698+
const_cache: RefCell::new(FxHashMap::default()),
680699
}
681700
}
682701

@@ -857,6 +876,7 @@ impl VisitProvenance for MiriMachine<'_, '_> {
857876
stack_size: _,
858877
collect_leak_backtraces: _,
859878
allocation_spans: _,
879+
const_cache: _,
860880
} = self;
861881

862882
threads.visit_provenance(visit);
@@ -1400,6 +1420,7 @@ impl<'mir, 'tcx> Machine<'mir, 'tcx> for MiriMachine<'mir, 'tcx> {
14001420
catch_unwind: None,
14011421
timing,
14021422
is_user_relevant: ecx.machine.is_user_relevant(&frame),
1423+
salt: ecx.machine.rng.borrow_mut().gen::<usize>() % ADDRS_PER_CONST,
14031424
};
14041425

14051426
Ok(frame.with_extra(extra))
@@ -1505,4 +1526,31 @@ impl<'mir, 'tcx> Machine<'mir, 'tcx> for MiriMachine<'mir, 'tcx> {
15051526
ecx.machine.allocation_spans.borrow_mut().insert(alloc_id, (span, None));
15061527
Ok(())
15071528
}
1529+
1530+
fn eval_mir_constant<F>(
1531+
ecx: &InterpCx<'mir, 'tcx, Self>,
1532+
val: mir::Const<'tcx>,
1533+
span: Option<Span>,
1534+
layout: Option<TyAndLayout<'tcx>>,
1535+
eval: F,
1536+
) -> InterpResult<'tcx, OpTy<'tcx, Self::Provenance>>
1537+
where
1538+
F: Fn(
1539+
&InterpCx<'mir, 'tcx, Self>,
1540+
mir::Const<'tcx>,
1541+
Option<Span>,
1542+
Option<TyAndLayout<'tcx>>,
1543+
) -> InterpResult<'tcx, OpTy<'tcx, Self::Provenance>>,
1544+
{
1545+
let frame = ecx.active_thread_stack().last().unwrap();
1546+
let mut cache = ecx.machine.const_cache.borrow_mut();
1547+
match cache.entry((val, frame.extra.salt)) {
1548+
Entry::Vacant(ve) => {
1549+
let op = eval(ecx, val, span, layout)?;
1550+
ve.insert(op.clone());
1551+
Ok(op)
1552+
}
1553+
Entry::Occupied(oe) => Ok(oe.get().clone()),
1554+
}
1555+
}
15081556
}
+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// The const fn interpreter creates a new AllocId every time it evaluates any const.
2+
// If we do that in Miri, repeatedly evaluating a const causes unbounded memory use
3+
// we need to keep track of the base address for that AllocId, and the allocation is never
4+
// deallocated.
5+
// In Miri we explicitly store previously-assigned AllocIds for each const and ensure
6+
// that we only hand out a finite number of AllocIds per const.
7+
// MIR inlining will put every evaluation of the const we're repeatedly evaluting into the same
8+
// stack frame, breaking this test.
9+
//@compile-flags: -Zinline-mir=no
10+
#![feature(strict_provenance)]
11+
12+
const EVALS: usize = 256;
13+
14+
use std::collections::HashSet;
15+
fn main() {
16+
let mut addrs = HashSet::new();
17+
for _ in 0..EVALS {
18+
addrs.insert(const_addr());
19+
}
20+
// Check that the const allocation has multiple base addresses
21+
assert!(addrs.len() > 1);
22+
// But also that we get a limited number of unique base addresses
23+
assert!(addrs.len() < EVALS);
24+
25+
// Check that within a call we always produce the same address
26+
let mut prev = 0;
27+
for iter in 0..EVALS {
28+
let addr = "test".as_bytes().as_ptr().addr();
29+
if iter > 0 {
30+
assert_eq!(prev, addr);
31+
}
32+
prev = addr;
33+
}
34+
}
35+
36+
fn const_addr() -> usize {
37+
"test".as_bytes().as_ptr().addr()
38+
}

0 commit comments

Comments
 (0)