Skip to content

Commit 30d0309

Browse files
committed
Auto merge of #148486 - kpreid:vec-iter-drop, r=jhpratt
Explicitly forget the zero remaining elements in `vec::IntoIter::fold()`. [Original description:] ~~This seems to help LLVM notice that dropping the elements in the destructor of `IntoIter` is not necessary. In cases it doesn’t help, it should be cheap since it is just one assignment.~~ This PR adds a function to `vec::IntoIter()` which is used used by `fold()` and `spec_extend()`, when those operations complete, to forget the zero remaining elements and only deallocate the allocation, ensuring that there will never be a useless loop to drop zero remaining elements when the iterator is dropped. This is my first ever attempt at this kind of codegen micro-optimization in the standard library, so please let me know what should go into the PR or what sort of additional systematic testing might indicate this is a good or bad idea.
2 parents e5efd33 + f700fdc commit 30d0309

4 files changed

Lines changed: 121 additions & 12 deletions

File tree

library/alloc/src/collections/vec_deque/spec_extend.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,15 +78,15 @@ where
7878

7979
#[cfg(not(test))]
8080
impl<T, A1: Allocator, A2: Allocator> SpecExtend<T, vec::IntoIter<T, A2>> for VecDeque<T, A1> {
81-
fn spec_extend(&mut self, mut iterator: vec::IntoIter<T, A2>) {
81+
fn spec_extend(&mut self, iterator: vec::IntoIter<T, A2>) {
8282
let slice = iterator.as_slice();
8383
self.reserve(slice.len());
8484

8585
unsafe {
8686
self.copy_slice(self.to_physical_idx(self.len), slice);
8787
self.len += slice.len();
8888
}
89-
iterator.forget_remaining_elements();
89+
iterator.forget_remaining_elements_and_dealloc();
9090
}
9191
}
9292

@@ -155,12 +155,12 @@ where
155155
#[cfg(not(test))]
156156
impl<T, A1: Allocator, A2: Allocator> SpecExtendFront<T, vec::IntoIter<T, A2>> for VecDeque<T, A1> {
157157
#[track_caller]
158-
fn spec_extend_front(&mut self, mut iterator: vec::IntoIter<T, A2>) {
158+
fn spec_extend_front(&mut self, iterator: vec::IntoIter<T, A2>) {
159159
let slice = iterator.as_slice();
160160
self.reserve(slice.len());
161161
// SAFETY: `slice.len()` space was just reserved and elements in the slice are forgotten after this call
162162
unsafe { prepend_reversed(self, slice) };
163-
iterator.forget_remaining_elements();
163+
iterator.forget_remaining_elements_and_dealloc();
164164
}
165165
}
166166

@@ -170,12 +170,12 @@ impl<T, A1: Allocator, A2: Allocator> SpecExtendFront<T, Rev<vec::IntoIter<T, A2
170170
{
171171
#[track_caller]
172172
fn spec_extend_front(&mut self, iterator: Rev<vec::IntoIter<T, A2>>) {
173-
let mut iterator = iterator.into_inner();
173+
let iterator = iterator.into_inner();
174174
let slice = iterator.as_slice();
175175
self.reserve(slice.len());
176176
// SAFETY: `slice.len()` space was just reserved and elements in the slice are forgotten after this call
177177
unsafe { prepend(self, slice) };
178-
iterator.forget_remaining_elements();
178+
iterator.forget_remaining_elements_and_dealloc();
179179
}
180180
}
181181

library/alloc/src/vec/into_iter.rs

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -159,12 +159,51 @@ impl<T, A: Allocator> IntoIter<T, A> {
159159
}
160160

161161
/// Forgets to Drop the remaining elements while still allowing the backing allocation to be freed.
162+
///
163+
/// This method does not consume `self`, and leaves deallocation to `impl Drop for IntoIter`.
164+
/// If consuming `self` is possible, consider calling
165+
/// [`Self::forget_remaining_elements_and_dealloc()`] instead.
162166
pub(crate) fn forget_remaining_elements(&mut self) {
163167
// For the ZST case, it is crucial that we mutate `end` here, not `ptr`.
164168
// `ptr` must stay aligned, while `end` may be unaligned.
165169
self.end = self.ptr.as_ptr();
166170
}
167171

172+
/// Forgets to Drop the remaining elements and frees the backing allocation.
173+
/// Consuming version of [`Self::forget_remaining_elements()`].
174+
///
175+
/// This can be used in place of `drop(self)` when `self` is known to be exhausted,
176+
/// to avoid producing a needless `drop_in_place::<[T]>()`.
177+
#[inline]
178+
pub(crate) fn forget_remaining_elements_and_dealloc(self) {
179+
let mut this = ManuallyDrop::new(self);
180+
// SAFETY: `this` is in ManuallyDrop, so it will not be double-freed.
181+
unsafe {
182+
this.dealloc_only();
183+
}
184+
}
185+
186+
/// Frees the allocation, without checking or dropping anything else.
187+
///
188+
/// The safe version of this method is [`Self::forget_remaining_elements_and_dealloc()`].
189+
/// This function exists only to share code between that method and the `impl Drop`.
190+
///
191+
/// # Safety
192+
///
193+
/// This function must only be called with an [`IntoIter`] that is not going to be dropped
194+
/// or otherwise used in any way, either because it is being forgotten or because its `Drop`
195+
/// is already executing; otherwise a double-free will occur, and possibly a read from freed
196+
/// memory if there are any remaining elements.
197+
#[inline]
198+
unsafe fn dealloc_only(&mut self) {
199+
unsafe {
200+
// SAFETY: our caller promises not to touch `*self` again
201+
let alloc = ManuallyDrop::take(&mut self.alloc);
202+
// RawVec handles deallocation
203+
let _ = RawVec::from_nonnull_in(self.buf, self.cap, alloc);
204+
}
205+
}
206+
168207
#[cfg(not(no_global_oom_handling))]
169208
#[inline]
170209
pub(crate) fn into_vecdeque(self) -> VecDeque<T, A> {
@@ -328,6 +367,12 @@ impl<T, A: Allocator> Iterator for IntoIter<T, A> {
328367
accum = f(accum, tmp);
329368
}
330369
}
370+
371+
// There are in fact no remaining elements to forget, but by doing this we can avoid
372+
// potentially generating a needless loop to drop the elements that cannot exist at
373+
// this point.
374+
self.forget_remaining_elements_and_dealloc();
375+
331376
accum
332377
}
333378

@@ -500,10 +545,7 @@ unsafe impl<#[may_dangle] T, A: Allocator> Drop for IntoIter<T, A> {
500545
impl<T, A: Allocator> Drop for DropGuard<'_, T, A> {
501546
fn drop(&mut self) {
502547
unsafe {
503-
// `IntoIter::alloc` is not used anymore after this and will be dropped by RawVec
504-
let alloc = ManuallyDrop::take(&mut self.0.alloc);
505-
// RawVec handles deallocation
506-
let _ = RawVec::from_nonnull_in(self.0.buf, self.0.cap, alloc);
548+
self.0.dealloc_only();
507549
}
508550
}
509551
}

library/alloc/src/vec/spec_extend.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@ where
2929
}
3030

3131
impl<T, A1: Allocator, A2: Allocator> SpecExtend<T, IntoIter<T, A2>> for Vec<T, A1> {
32-
fn spec_extend(&mut self, mut iterator: IntoIter<T, A2>) {
32+
fn spec_extend(&mut self, iterator: IntoIter<T, A2>) {
3333
unsafe {
3434
self.append_elements(iterator.as_slice() as _);
3535
}
36-
iterator.forget_remaining_elements();
36+
iterator.forget_remaining_elements_and_dealloc();
3737
}
3838
}
3939

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
//@ compile-flags: -Copt-level=3
2+
// Test that we can avoid generating an element dropping loop when `vec::IntoIter` is consumed.
3+
#![crate_type = "lib"]
4+
5+
use std::vec;
6+
7+
struct Bomb;
8+
impl Drop for Bomb {
9+
#[inline]
10+
fn drop(&mut self) {
11+
panic!("dropped")
12+
}
13+
}
14+
15+
/// Test case originally from https://users.rust-lang.org/t/unnecessary-drop-in-place-emitted-for-a-fully-consumed-intoiter/135119
16+
///
17+
/// What we are looking for is that there should be no calls to `impl Drop for Bomb`
18+
/// because every element is unconditionally forgotten.
19+
//
20+
// CHECK-LABEL: @vec_for_each_doesnt_drop
21+
#[no_mangle]
22+
pub fn vec_for_each_doesnt_drop(v: vec::Vec<(usize, Option<Bomb>)>) -> usize {
23+
// CHECK-NOT: panic
24+
// CHECK-NOT: drop_in_place
25+
// CHECK-NOT: Bomb$u20$as$u20$core..ops..drop..Drop
26+
let mut last = 0;
27+
v.into_iter().for_each(|(x, bomb)| {
28+
last = x;
29+
std::mem::forget(bomb);
30+
});
31+
last
32+
}
33+
34+
/// Test that does *not* get the above optimization we are expecting:
35+
/// it uses a normal `for` loop which calls `Iterator::next()` and then drops the iterator,
36+
/// and dropping the iterator drops remaining items.
37+
///
38+
/// This test exists to prove that the above CHECK-NOT is looking for the right things.
39+
/// However, it might start failing if LLVM figures out that there are no remaining items.
40+
//
41+
// CHECK-LABEL: @vec_for_loop
42+
#[no_mangle]
43+
pub fn vec_for_loop(v: vec::Vec<(usize, Option<Bomb>)>) -> usize {
44+
// CHECK: drop_in_place
45+
let mut last = 0;
46+
for (x, bomb) in v {
47+
last = x;
48+
std::mem::forget(bomb);
49+
}
50+
last
51+
}
52+
53+
/// Test where there still should be drops because there are no forgets.
54+
///
55+
/// This test exists to prove that the above CHECK-NOT is looking for the right things
56+
/// and does not say anything interesting about codegen itself.
57+
//
58+
// CHECK-LABEL: @vec_for_each_does_drop
59+
#[no_mangle]
60+
pub fn vec_for_each_does_drop(v: vec::Vec<(usize, Option<Bomb>)>) -> usize {
61+
// CHECK: begin_panic
62+
let mut last = 0;
63+
v.into_iter().for_each(|(x, bomb)| {
64+
last = x;
65+
});
66+
last
67+
}

0 commit comments

Comments
 (0)