Skip to content

Commit 6b96d17

Browse files
Dumb NRVO
1 parent dd927a5 commit 6b96d17

File tree

2 files changed

+221
-0
lines changed

2 files changed

+221
-0
lines changed

src/librustc_mir/transform/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ pub mod generator;
2828
pub mod inline;
2929
pub mod instcombine;
3030
pub mod no_landing_pads;
31+
pub mod nrvo;
3132
pub mod promote_consts;
3233
pub mod qualify_min_const_fn;
3334
pub mod remove_noop_landing_pads;
@@ -324,6 +325,7 @@ fn run_optimization_passes<'tcx>(
324325
&remove_noop_landing_pads::RemoveNoopLandingPads,
325326
&simplify::SimplifyCfg::new("after-remove-noop-landing-pads"),
326327
&simplify::SimplifyCfg::new("final"),
328+
&nrvo::RenameReturnPlace,
327329
&simplify::SimplifyLocals,
328330
];
329331

src/librustc_mir/transform/nrvo.rs

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
use rustc_index::bit_set::HybridBitSet;
2+
use rustc_middle::mir::visit::{MutVisitor, PlaceContext, Visitor};
3+
use rustc_middle::mir::{self, BasicBlock, Local, Location};
4+
use rustc_middle::ty::TyCtxt;
5+
6+
use crate::transform::{MirPass, MirSource};
7+
8+
/// This pass looks for MIR that always copies the same local into the return place and eliminates
9+
/// the copy by renaming all uses of that local to `_0`.
10+
///
11+
/// This allows LLVM to perform an optimization similar to the named return value optimization
12+
/// (NRVO) that is guaranteed in C++. This avoids a stack allocation and `memcpy` for the
13+
/// relatively common pattern of allocating a buffer on the stack, mutating it, and returning it by
14+
/// value like so:
15+
///
16+
/// ```rust
17+
/// fn foo(init: fn(&mut [u8; 1024])) -> [u8; 1024] {
18+
/// let mut buf = [0; 1024];
19+
/// init(&mut buf);
20+
/// buf
21+
/// }
22+
/// ```
23+
///
24+
/// For now, this pass is very simple and only capable of eliminating a single copy.
25+
/// A more general version of copy propagation could yield even more benefits.
26+
pub struct RenameReturnPlace;
27+
28+
impl<'tcx> MirPass<'tcx> for RenameReturnPlace {
29+
fn run_pass(&self, tcx: TyCtxt<'tcx>, src: MirSource<'tcx>, body: &mut mir::Body<'tcx>) {
30+
if tcx.sess.opts.debugging_opts.mir_opt_level == 0 {
31+
return;
32+
}
33+
34+
let returned_local = match local_eligible_for_nrvo(body) {
35+
Some(l) => l,
36+
None => {
37+
debug!("`{:?}` was ineligible for NRVO", src.def_id());
38+
return;
39+
}
40+
};
41+
42+
debug!(
43+
"`{:?}` was eligible for NRVO, making {:?} the return place",
44+
src.def_id(),
45+
returned_local
46+
);
47+
48+
RenameToReturnPlace { tcx, to_rename: returned_local }.visit_body(body);
49+
50+
// Clean up the `NOP`s we inserted for statements made useless by our renaming.
51+
for block_data in body.basic_blocks_mut() {
52+
block_data.statements.retain(|stmt| stmt.kind != mir::StatementKind::Nop);
53+
}
54+
55+
// Overwrite the debuginfo of `_0` with that of the renamed local.
56+
let (renamed_decl, ret_decl) =
57+
body.local_decls.pick2_mut(returned_local, mir::RETURN_PLACE);
58+
debug_assert_eq!(ret_decl.ty, renamed_decl.ty);
59+
ret_decl.clone_from(renamed_decl);
60+
}
61+
}
62+
63+
/// MIR that is eligible for the NRVO must fulfill two conditions:
64+
/// 1. The return place must not be read prior to the `Return` terminator.
65+
/// 2. A simple assignment of a whole local to the return place (e.g., `_0 = _1`) must be the
66+
/// only definition of the return place reaching the `Return` terminator.
67+
///
68+
/// If the MIR fulfills both these conditions, this function returns the `Local` that is assigned
69+
/// to the return place along all possible paths through the control-flow graph.
70+
fn local_eligible_for_nrvo(body: &mut mir::Body<'_>) -> Option<Local> {
71+
if IsReturnPlaceRead::run(body) {
72+
return None;
73+
}
74+
75+
let mut copied_to_return_place = None;
76+
for block in body.basic_blocks().indices() {
77+
// Look for blocks with a `Return` terminator.
78+
if !matches!(body[block].terminator().kind, mir::TerminatorKind::Return) {
79+
continue;
80+
}
81+
82+
// Look for an assignment of a single local to the return place prior to the `Return`.
83+
let returned_local = find_local_assigned_to_return_place(block, body)?;
84+
match body.local_kind(returned_local) {
85+
// FIXME: Can we do this for arguments as well?
86+
mir::LocalKind::Arg => return None,
87+
88+
mir::LocalKind::ReturnPointer => bug!("Return place was assigned to itself?"),
89+
mir::LocalKind::Var | mir::LocalKind::Temp => {}
90+
}
91+
92+
// If multiple different locals are copied to the return place. We can't pick a
93+
// single one to rename.
94+
if copied_to_return_place.map_or(false, |old| old != returned_local) {
95+
return None;
96+
}
97+
98+
copied_to_return_place = Some(returned_local);
99+
}
100+
101+
return copied_to_return_place;
102+
}
103+
104+
fn find_local_assigned_to_return_place(
105+
start: BasicBlock,
106+
body: &mut mir::Body<'_>,
107+
) -> Option<Local> {
108+
let mut block = start;
109+
let mut seen = HybridBitSet::new_empty(body.basic_blocks().len());
110+
111+
// Iterate as long as `block` has exactly one predecessor that we have not yet visited.
112+
while seen.insert(block) {
113+
trace!("Looking for assignments to `_0` in {:?}", block);
114+
115+
let local = body[block].statements.iter().rev().find_map(as_local_assigned_to_return_place);
116+
if local.is_some() {
117+
return local;
118+
}
119+
120+
match body.predecessors()[block].as_slice() {
121+
&[pred] => block = pred,
122+
_ => return None,
123+
}
124+
}
125+
126+
return None;
127+
}
128+
129+
// If this statement is an assignment of an unprojected local to the return place,
130+
// return that local.
131+
fn as_local_assigned_to_return_place(stmt: &mir::Statement<'_>) -> Option<Local> {
132+
if let mir::StatementKind::Assign(box (lhs, rhs)) = &stmt.kind {
133+
if lhs.as_local() == Some(mir::RETURN_PLACE) {
134+
if let mir::Rvalue::Use(mir::Operand::Copy(rhs) | mir::Operand::Move(rhs)) = rhs {
135+
return rhs.as_local();
136+
}
137+
}
138+
}
139+
140+
None
141+
}
142+
143+
struct RenameToReturnPlace<'tcx> {
144+
to_rename: Local,
145+
tcx: TyCtxt<'tcx>,
146+
}
147+
148+
/// Replaces all uses of `self.to_rename` with `_0`.
149+
impl MutVisitor<'tcx> for RenameToReturnPlace<'tcx> {
150+
fn tcx(&self) -> TyCtxt<'tcx> {
151+
self.tcx
152+
}
153+
154+
fn visit_statement(&mut self, stmt: &mut mir::Statement<'tcx>, loc: Location) {
155+
// Remove assignments of the local being replaced to the return place, since it is now the
156+
// return place:
157+
// _0 = _1
158+
if as_local_assigned_to_return_place(stmt) == Some(self.to_rename) {
159+
stmt.kind = mir::StatementKind::Nop;
160+
return;
161+
}
162+
163+
// Remove storage annotations for the local being replaced:
164+
// StorageLive(_1)
165+
if let mir::StatementKind::StorageLive(local) | mir::StatementKind::StorageDead(local) =
166+
stmt.kind
167+
{
168+
if local == self.to_rename {
169+
stmt.kind = mir::StatementKind::Nop;
170+
return;
171+
}
172+
}
173+
174+
self.super_statement(stmt, loc)
175+
}
176+
177+
fn visit_terminator(&mut self, terminator: &mut mir::Terminator<'tcx>, loc: Location) {
178+
// Ignore the implicit "use" of the return place in a `Return` statement.
179+
if let mir::TerminatorKind::Return = terminator.kind {
180+
return;
181+
}
182+
183+
self.super_terminator(terminator, loc);
184+
}
185+
186+
fn visit_local(&mut self, l: &mut Local, _: PlaceContext, _: Location) {
187+
assert_ne!(*l, mir::RETURN_PLACE);
188+
if *l == self.to_rename {
189+
*l = mir::RETURN_PLACE;
190+
}
191+
}
192+
}
193+
194+
struct IsReturnPlaceRead(bool);
195+
196+
impl IsReturnPlaceRead {
197+
fn run(body: &mir::Body<'_>) -> bool {
198+
let mut vis = IsReturnPlaceRead(false);
199+
vis.visit_body(body);
200+
vis.0
201+
}
202+
}
203+
204+
impl Visitor<'tcx> for IsReturnPlaceRead {
205+
fn visit_local(&mut self, &l: &Local, ctxt: PlaceContext, _: Location) {
206+
if l == mir::RETURN_PLACE && ctxt.is_use() && !ctxt.is_place_assignment() {
207+
self.0 = true;
208+
}
209+
}
210+
211+
fn visit_terminator(&mut self, terminator: &mir::Terminator<'tcx>, loc: Location) {
212+
// Ignore the implicit "use" of the return place in a `Return` statement.
213+
if let mir::TerminatorKind::Return = terminator.kind {
214+
return;
215+
}
216+
217+
self.super_terminator(terminator, loc);
218+
}
219+
}

0 commit comments

Comments
 (0)