Auto merge of #69716 - jonas-schievink:generator-size, r=tmandry

Don't store locals in generators that are immediately overwritten with the resume argument

This fixes https://github.com/rust-lang/rust/issues/69672 and makes https://github.com/rust-lang/rust/pull/69033 pass the async fn size tests again (in other words, there will be no size regression of async fn if both this and https://github.com/rust-lang/rust/pull/69033 land).

~~This is a small botch and I'd rather have a more precise analysis, but that seems much harder to pull off, so this special-cases `Yield` terminators that store the resume argument into a simple local (ie. without any field projections) and explicitly marks that local as "not live" in the suspend point of that yield. We know that this local does not need to be stored in the generator for this suspend point because the next resume would immediately overwrite it with the passed-in resume argument anyways. The local might still end up in the state if it is used across another yield.~~ (this now properly updates the dataflow framework to handle this case)
This commit is contained in:
bors 2020-03-14 02:04:49 +00:00
commit 5ed3453af9
11 changed files with 120 additions and 31 deletions

View File

@ -2,7 +2,7 @@
use std::borrow::Borrow; use std::borrow::Borrow;
use rustc::mir::{self, BasicBlock, Location}; use rustc::mir::{self, BasicBlock, Location, TerminatorKind};
use rustc_index::bit_set::BitSet; use rustc_index::bit_set::BitSet;
use super::{Analysis, Results}; use super::{Analysis, Results};
@ -29,14 +29,14 @@ where
pos: CursorPosition, pos: CursorPosition,
/// When this flag is set, the cursor is pointing at a `Call` terminator whose call return /// When this flag is set, the cursor is pointing at a `Call` or `Yield` terminator whose call
/// effect has been applied to `state`. /// return or resume effect has been applied to `state`.
/// ///
/// This flag helps to ensure that multiple calls to `seek_after_assume_call_returns` with the /// This flag helps to ensure that multiple calls to `seek_after_assume_success` with the
/// same target will result in exactly one invocation of `apply_call_return_effect`. It is /// same target will result in exactly one invocation of `apply_call_return_effect`. It is
/// sufficient to clear this only in `seek_to_block_start`, since seeking away from a /// sufficient to clear this only in `seek_to_block_start`, since seeking away from a
/// terminator will always require a cursor reset. /// terminator will always require a cursor reset.
call_return_effect_applied: bool, success_effect_applied: bool,
} }
impl<'mir, 'tcx, A, R> ResultsCursor<'mir, 'tcx, A, R> impl<'mir, 'tcx, A, R> ResultsCursor<'mir, 'tcx, A, R>
@ -50,7 +50,7 @@ where
body, body,
pos: CursorPosition::BlockStart(mir::START_BLOCK), pos: CursorPosition::BlockStart(mir::START_BLOCK),
state: results.borrow().entry_sets[mir::START_BLOCK].clone(), state: results.borrow().entry_sets[mir::START_BLOCK].clone(),
call_return_effect_applied: false, success_effect_applied: false,
results, results,
} }
} }
@ -76,14 +76,14 @@ where
pub fn seek_to_block_start(&mut self, block: BasicBlock) { pub fn seek_to_block_start(&mut self, block: BasicBlock) {
self.state.overwrite(&self.results.borrow().entry_sets[block]); self.state.overwrite(&self.results.borrow().entry_sets[block]);
self.pos = CursorPosition::BlockStart(block); self.pos = CursorPosition::BlockStart(block);
self.call_return_effect_applied = false; self.success_effect_applied = false;
} }
/// Advances the cursor to hold all effects up to and including to the "before" effect of the /// Advances the cursor to hold all effects up to and including to the "before" effect of the
/// statement (or terminator) at the given location. /// statement (or terminator) at the given location.
/// ///
/// If you wish to observe the full effect of a statement or terminator, not just the "before" /// If you wish to observe the full effect of a statement or terminator, not just the "before"
/// effect, use `seek_after` or `seek_after_assume_call_returns`. /// effect, use `seek_after` or `seek_after_assume_success`.
pub fn seek_before(&mut self, target: Location) { pub fn seek_before(&mut self, target: Location) {
assert!(target <= self.body.terminator_loc(target.block)); assert!(target <= self.body.terminator_loc(target.block));
self.seek_(target, false); self.seek_(target, false);
@ -93,7 +93,7 @@ where
/// terminators) up to and including the `target`. /// terminators) up to and including the `target`.
/// ///
/// If the `target` is a `Call` terminator, any call return effect for that terminator will /// If the `target` is a `Call` terminator, any call return effect for that terminator will
/// **not** be observed. Use `seek_after_assume_call_returns` if you wish to observe the call /// **not** be observed. Use `seek_after_assume_success` if you wish to observe the call
/// return effect. /// return effect.
pub fn seek_after(&mut self, target: Location) { pub fn seek_after(&mut self, target: Location) {
assert!(target <= self.body.terminator_loc(target.block)); assert!(target <= self.body.terminator_loc(target.block));
@ -101,7 +101,7 @@ where
// If we have already applied the call return effect, we are currently pointing at a `Call` // If we have already applied the call return effect, we are currently pointing at a `Call`
// terminator. Unconditionally reset the dataflow cursor, since there is no way to "undo" // terminator. Unconditionally reset the dataflow cursor, since there is no way to "undo"
// the call return effect. // the call return effect.
if self.call_return_effect_applied { if self.success_effect_applied {
self.seek_to_block_start(target.block); self.seek_to_block_start(target.block);
} }
@ -111,25 +111,25 @@ where
/// Advances the cursor to hold all effects up to and including of the statement (or /// Advances the cursor to hold all effects up to and including of the statement (or
/// terminator) at the given location. /// terminator) at the given location.
/// ///
/// If the `target` is a `Call` terminator, any call return effect for that terminator will /// If the `target` is a `Call` or `Yield` terminator, any call return or resume effect for that
/// be observed. Use `seek_after` if you do **not** wish to observe the call return effect. /// terminator will be observed. Use `seek_after` if you do **not** wish to observe the
pub fn seek_after_assume_call_returns(&mut self, target: Location) { /// "success" effect.
pub fn seek_after_assume_success(&mut self, target: Location) {
let terminator_loc = self.body.terminator_loc(target.block); let terminator_loc = self.body.terminator_loc(target.block);
assert!(target.statement_index <= terminator_loc.statement_index); assert!(target.statement_index <= terminator_loc.statement_index);
self.seek_(target, true); self.seek_(target, true);
if target != terminator_loc { if target != terminator_loc || self.success_effect_applied {
return; return;
} }
// Apply the effect of the "success" path of the terminator.
self.success_effect_applied = true;
let terminator = self.body.basic_blocks()[target.block].terminator(); let terminator = self.body.basic_blocks()[target.block].terminator();
if let mir::TerminatorKind::Call { match &terminator.kind {
destination: Some((return_place, _)), func, args, .. TerminatorKind::Call { destination: Some((return_place, _)), func, args, .. } => {
} = &terminator.kind
{
if !self.call_return_effect_applied {
self.call_return_effect_applied = true;
self.results.borrow().analysis.apply_call_return_effect( self.results.borrow().analysis.apply_call_return_effect(
&mut self.state, &mut self.state,
target.block, target.block,
@ -138,6 +138,14 @@ where
return_place, return_place,
); );
} }
TerminatorKind::Yield { resume, resume_arg, .. } => {
self.results.borrow().analysis.apply_yield_resume_effect(
&mut self.state,
*resume,
resume_arg,
);
}
_ => {}
} }
} }
@ -172,7 +180,7 @@ where
self.seek_to_block_start(target.block) self.seek_to_block_start(target.block)
} }
// N.B., `call_return_effect_applied` is checked in `seek_after`, not here. // N.B., `success_effect_applied` is checked in `seek_after`, not here.
_ => (), _ => (),
} }

View File

@ -218,15 +218,18 @@ where
Goto { target } Goto { target }
| Assert { target, cleanup: None, .. } | Assert { target, cleanup: None, .. }
| Yield { resume: target, drop: None, .. }
| Drop { target, location: _, unwind: None } | Drop { target, location: _, unwind: None }
| DropAndReplace { target, value: _, location: _, unwind: None } => { | DropAndReplace { target, value: _, location: _, unwind: None } => {
self.propagate_bits_into_entry_set_for(in_out, target, dirty_list) self.propagate_bits_into_entry_set_for(in_out, target, dirty_list)
} }
Yield { resume: target, drop: Some(drop), .. } => { Yield { resume: target, drop, resume_arg, .. } => {
if let Some(drop) = drop {
self.propagate_bits_into_entry_set_for(in_out, drop, dirty_list);
}
self.analysis.apply_yield_resume_effect(in_out, target, &resume_arg);
self.propagate_bits_into_entry_set_for(in_out, target, dirty_list); self.propagate_bits_into_entry_set_for(in_out, target, dirty_list);
self.propagate_bits_into_entry_set_for(in_out, drop, dirty_list);
} }
Assert { target, cleanup: Some(unwind), .. } Assert { target, cleanup: Some(unwind), .. }

View File

@ -241,7 +241,7 @@ where
)?; )?;
let state_on_unwind = this.results.get().clone(); let state_on_unwind = this.results.get().clone();
this.results.seek_after_assume_call_returns(terminator_loc); this.results.seek_after_assume_success(terminator_loc);
write_diff(w, this.results.analysis(), &state_on_unwind, this.results.get())?; write_diff(w, this.results.analysis(), &state_on_unwind, this.results.get())?;
write!(w, "</td>") write!(w, "</td>")

View File

@ -191,6 +191,20 @@ pub trait Analysis<'tcx>: AnalysisDomain<'tcx> {
return_place: &mir::Place<'tcx>, return_place: &mir::Place<'tcx>,
); );
/// Updates the current dataflow state with the effect of resuming from a `Yield` terminator.
///
/// This is similar to `apply_call_return_effect` in that it only takes place after the
/// generator is resumed, not when it is dropped.
///
/// By default, no effects happen.
fn apply_yield_resume_effect(
&self,
_state: &mut BitSet<Self::Idx>,
_resume_block: BasicBlock,
_resume_place: &mir::Place<'tcx>,
) {
}
/// Updates the current dataflow state with the effect of taking a particular branch in a /// Updates the current dataflow state with the effect of taking a particular branch in a
/// `SwitchInt` terminator. /// `SwitchInt` terminator.
/// ///
@ -284,6 +298,15 @@ pub trait GenKillAnalysis<'tcx>: Analysis<'tcx> {
return_place: &mir::Place<'tcx>, return_place: &mir::Place<'tcx>,
); );
/// See `Analysis::apply_yield_resume_effect`.
fn yield_resume_effect(
&self,
_trans: &mut BitSet<Self::Idx>,
_resume_block: BasicBlock,
_resume_place: &mir::Place<'tcx>,
) {
}
/// See `Analysis::apply_discriminant_switch_effect`. /// See `Analysis::apply_discriminant_switch_effect`.
fn discriminant_switch_effect( fn discriminant_switch_effect(
&self, &self,
@ -347,6 +370,15 @@ where
self.call_return_effect(state, block, func, args, return_place); self.call_return_effect(state, block, func, args, return_place);
} }
fn apply_yield_resume_effect(
&self,
state: &mut BitSet<Self::Idx>,
resume_block: BasicBlock,
resume_place: &mir::Place<'tcx>,
) {
self.yield_resume_effect(state, resume_block, resume_place);
}
fn apply_discriminant_switch_effect( fn apply_discriminant_switch_effect(
&self, &self,
state: &mut BitSet<Self::Idx>, state: &mut BitSet<Self::Idx>,

View File

@ -294,7 +294,7 @@ fn cursor_seek() {
cursor.seek_after(call_terminator_loc); cursor.seek_after(call_terminator_loc);
assert!(!cursor.get().contains(call_return_effect)); assert!(!cursor.get().contains(call_return_effect));
cursor.seek_after_assume_call_returns(call_terminator_loc); cursor.seek_after_assume_success(call_terminator_loc);
assert!(cursor.get().contains(call_return_effect)); assert!(cursor.get().contains(call_return_effect));
let every_target = || { let every_target = || {
@ -310,7 +310,7 @@ fn cursor_seek() {
BlockStart(block) => cursor.seek_to_block_start(block), BlockStart(block) => cursor.seek_to_block_start(block),
Before(loc) => cursor.seek_before(loc), Before(loc) => cursor.seek_before(loc),
After(loc) => cursor.seek_after(loc), After(loc) => cursor.seek_after(loc),
AfterAssumeCallReturns(loc) => cursor.seek_after_assume_call_returns(loc), AfterAssumeCallReturns(loc) => cursor.seek_after_assume_success(loc),
} }
assert_eq!(cursor.get(), &cursor.analysis().expected_state_at_target(targ)); assert_eq!(cursor.get(), &cursor.analysis().expected_state_at_target(targ));

View File

@ -161,11 +161,16 @@ impl<'mir, 'tcx> dataflow::GenKillAnalysis<'tcx> for MaybeRequiresStorage<'mir,
self.borrowed_locals.borrow().analysis().terminator_effect(trans, terminator, loc); self.borrowed_locals.borrow().analysis().terminator_effect(trans, terminator, loc);
match &terminator.kind { match &terminator.kind {
TerminatorKind::Call { destination: Some((place, _)), .. } TerminatorKind::Call { destination: Some((place, _)), .. } => {
| TerminatorKind::Yield { resume_arg: place, .. } => {
trans.gen(place.local); trans.gen(place.local);
} }
// Note that we do *not* gen the `resume_arg` of `Yield` terminators. The reason for
// that is that a `yield` will return from the function, and `resume_arg` is written
// only when the generator is later resumed. Unlike `Call`, this doesn't require the
// place to have storage *before* the yield, only after.
TerminatorKind::Yield { .. } => {}
// Nothing to do for these. Match exhaustively so this fails to compile when new // Nothing to do for these. Match exhaustively so this fails to compile when new
// variants are added. // variants are added.
TerminatorKind::Call { destination: None, .. } TerminatorKind::Call { destination: None, .. }
@ -230,6 +235,15 @@ impl<'mir, 'tcx> dataflow::GenKillAnalysis<'tcx> for MaybeRequiresStorage<'mir,
) { ) {
trans.gen(return_place.local); trans.gen(return_place.local);
} }
fn yield_resume_effect(
&self,
trans: &mut BitSet<Self::Idx>,
_resume_block: BasicBlock,
resume_place: &mir::Place<'tcx>,
) {
trans.gen(resume_place.local);
}
} }
impl<'mir, 'tcx> MaybeRequiresStorage<'mir, 'tcx> { impl<'mir, 'tcx> MaybeRequiresStorage<'mir, 'tcx> {

View File

@ -539,7 +539,7 @@ fn locals_live_across_suspend_points(
let mut live_locals_here = storage_required; let mut live_locals_here = storage_required;
live_locals_here.intersect(&liveness.outs[block]); live_locals_here.intersect(&liveness.outs[block]);
// The generator argument is ignored // The generator argument is ignored.
live_locals_here.remove(self_arg()); live_locals_here.remove(self_arg());
debug!("loc = {:?}, live_locals_here = {:?}", loc, live_locals_here); debug!("loc = {:?}, live_locals_here = {:?}", loc, live_locals_here);

View File

@ -4,11 +4,15 @@
use std::ops::{Generator, GeneratorState}; use std::ops::{Generator, GeneratorState};
fn mkstr(my_name: String, my_mood: String) -> String {
format!("{} is {}", my_name.trim(), my_mood.trim())
}
fn my_scenario() -> impl Generator<String, Yield = &'static str, Return = String> { fn my_scenario() -> impl Generator<String, Yield = &'static str, Return = String> {
|_arg: String| { |_arg: String| {
let my_name = yield "What is your name?"; let my_name = yield "What is your name?";
let my_mood = yield "How are you feeling?"; let my_mood = yield "How are you feeling?";
format!("{} is {}", my_name.trim(), my_mood.trim()) mkstr(my_name, my_mood)
} }
} }

View File

@ -0,0 +1,28 @@
#![feature(generators)]
// run-pass
use std::mem::size_of_val;
fn main() {
// Generator taking a `Copy`able resume arg.
let gen_copy = |mut x: usize| {
loop {
drop(x);
x = yield;
}
};
// Generator taking a non-`Copy` resume arg.
let gen_move = |mut x: Box<usize>| {
loop {
drop(x);
x = yield;
}
};
// Neither of these generators have the resume arg live across the `yield`, so they should be
// 4 Bytes in size (only storing the discriminant)
assert_eq!(size_of_val(&gen_copy), 4);
assert_eq!(size_of_val(&gen_move), 4);
}