Skip to content

Commit 6a91333

Browse files
authored
Adds Vec::extend_from_slices_copy that accepts multiple slices (#240)
* Adds `Vec::extend_from_slices_copy` that accepts multiple slices * Adds quickchecks, tweaks comments and variable names * Adds cfg(feature="collections") to all new quickchecks * Renames variable `buf` to `slice` * Updated comment * Moves `set_len` call to `extend_from_slice_copy_unchecked` --------- Co-authored-by: Zack Slayton <[email protected]>
1 parent 2ed8718 commit 6a91333

File tree

4 files changed

+277
-18
lines changed

4 files changed

+277
-18
lines changed

benches/benches.rs

+73
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,78 @@ fn bench_extend_from_slice_copy(c: &mut Criterion) {
176176
}
177177
}
178178

179+
fn bench_extend_from_slices_copy(c: &mut Criterion) {
180+
// The number of slices that will be copied into the Vec
181+
let slice_counts = &[1, 2, 4, 8, 16, 32];
182+
183+
// Whether the Bump and its Vec have will already enough space to store the data without
184+
// requiring reallocation
185+
let is_preallocated_settings = &[false, true];
186+
187+
// Slices that can be used to extend the Vec; each may be used more than once.
188+
let data: [&[u8]; 4] = [
189+
black_box(b"wwwwwwwwwwwwwwww"),
190+
black_box(b"xxxxxxxxxxxxxxxx"),
191+
black_box(b"yyyyyyyyyyyyyyyy"),
192+
black_box(b"zzzzzzzzzzzzzzzz"),
193+
];
194+
195+
// For each (`is_preallocated`, `num_slices`) pair...
196+
for is_preallocated in is_preallocated_settings {
197+
for num_slices in slice_counts.iter().copied() {
198+
// Create an appropriately named benchmark group
199+
let mut group = c.benchmark_group(
200+
format!("extend_from_slices num_slices={num_slices}, is_preallocated={is_preallocated}")
201+
);
202+
203+
// Cycle over `data` to construct a slice of slices to append
204+
let slices = data
205+
.iter()
206+
.copied()
207+
.cycle()
208+
.take(num_slices)
209+
.collect::<Vec<_>>();
210+
let total_size = slices.iter().map(|s| s.len()).sum();
211+
212+
// If `is_preallocated` is true, both the Bump and the benchmark Vecs will have enough
213+
// capacity to store the concatenated data. If it's false, the Bump and the Vec start
214+
// out with no capacity allocated and grow on demand.
215+
let size_to_allocate = match is_preallocated {
216+
true => total_size,
217+
false => 0,
218+
};
219+
let mut bump = bumpalo::Bump::with_capacity(size_to_allocate);
220+
221+
// This benchmark demonstrates the performance of looping over the slice-of-slices,
222+
// calling `extend_from_slice_copy` (and transitively, `reserve`) for each slice.
223+
group.bench_function("loop over extend_from_slice_copy", |b| {
224+
b.iter(|| {
225+
bump.reset();
226+
let mut vec = bumpalo::collections::Vec::<u8>::with_capacity_in(size_to_allocate, &bump);
227+
for slice in black_box(&slices) {
228+
vec.extend_from_slice_copy(slice);
229+
}
230+
black_box(vec.as_slice());
231+
});
232+
});
233+
234+
// This benchmark demonstrates the performance of using a single call to
235+
// `extend_from_slices_copy`, which performs a single `reserve` before appending
236+
// all of the slices.
237+
group.bench_function("extend_from_slices_copy", |b| {
238+
b.iter(|| {
239+
bump.reset();
240+
let mut vec = bumpalo::collections::Vec::<u8>::with_capacity_in(size_to_allocate, &bump);
241+
vec.extend_from_slices_copy(black_box(slices.as_slice()));
242+
black_box(vec.as_slice());
243+
});
244+
});
245+
246+
group.finish();
247+
}
248+
}
249+
}
250+
179251
fn bench_alloc(c: &mut Criterion) {
180252
let mut group = c.benchmark_group("alloc");
181253
group.throughput(Throughput::Elements(ALLOCATIONS as u64));
@@ -320,6 +392,7 @@ fn bench_string_push_str(c: &mut Criterion) {
320392
criterion_group!(
321393
benches,
322394
bench_extend_from_slice_copy,
395+
bench_extend_from_slices_copy,
323396
bench_alloc,
324397
bench_alloc_with,
325398
bench_alloc_try_with,

src/collections/vec.rs

+83-18
Original file line numberDiff line numberDiff line change
@@ -1778,12 +1778,43 @@ impl<'bump, T: 'bump + Clone> Vec<'bump, T> {
17781778
}
17791779

17801780
impl<'bump, T: 'bump + Copy> Vec<'bump, T> {
1781+
/// Helper method to copy all of the items in `other` and append them to the end of `self`.
1782+
///
1783+
/// SAFETY:
1784+
/// * The caller is responsible for:
1785+
/// * calling [`reserve`](Self::reserve) beforehand to guarantee that there is enough
1786+
/// capacity to store `other.len()` more items.
1787+
/// * guaranteeing that `self` and `other` do not overlap.
1788+
unsafe fn extend_from_slice_copy_unchecked(&mut self, other: &[T]) {
1789+
let old_len = self.len();
1790+
debug_assert!(old_len + other.len() <= self.capacity());
1791+
1792+
// SAFETY:
1793+
// * `src` is valid for reads of `other.len()` values by virtue of being a `&[T]`.
1794+
// * `dst` is valid for writes of `other.len()` bytes because the caller of this
1795+
// method is required to `reserve` capacity to store at least `other.len()` items
1796+
// beforehand.
1797+
// * Because `src` is a `&[T]` and dst is a `&[T]` within the `Vec<T>`,
1798+
// `copy_nonoverlapping`'s alignment requirements are met.
1799+
// * Caller is required to guarantee that the source and destination ranges cannot overlap
1800+
unsafe {
1801+
let src = other.as_ptr();
1802+
let dst = self.as_mut_ptr().add(old_len);
1803+
ptr::copy_nonoverlapping(src, dst, other.len());
1804+
self.set_len(old_len + other.len());
1805+
}
1806+
}
1807+
1808+
17811809
/// Copies all elements in the slice `other` and appends them to the `Vec`.
17821810
///
17831811
/// Note that this function is same as [`extend_from_slice`] except that it is optimized for
17841812
/// slices of types that implement the `Copy` trait. If and when Rust gets specialization
17851813
/// this function will likely be deprecated (but still available).
17861814
///
1815+
/// To copy and append the data from multiple source slices at once, see
1816+
/// [`extend_from_slices_copy`].
1817+
///
17871818
/// # Examples
17881819
///
17891820
/// ```
@@ -1806,35 +1837,69 @@ impl<'bump, T: 'bump + Copy> Vec<'bump, T> {
18061837
/// assert_eq!(vec, "Hello, world!".as_bytes());
18071838
/// ```
18081839
///
1809-
/// [`extend`]: #method.extend_from_slice
1840+
/// [`extend_from_slice`]: #method.extend_from_slice
1841+
/// [`extend_from_slices`]: #method.extend_from_slices
18101842
pub fn extend_from_slice_copy(&mut self, other: &[T]) {
1811-
18121843
// Reserve space in the Vec for the values to be added
1813-
let old_len = self.len();
18141844
self.reserve(other.len());
18151845

1816-
let new_len = old_len + other.len();
1817-
debug_assert!(new_len <= self.capacity());
1818-
18191846
// Copy values into the space that was just reserved
18201847
// SAFETY:
1821-
// * `src` is valid for reads of `other.len()` values by virtue of being a `&[T]`.
1822-
// * `dst` is valid for writes of `other.len()` bytes as `self.reserve(other.len())`
1848+
// * `self` has enough capacity to store `other.len()` more items as `self.reserve(other.len())`
18231849
// above guarantees that.
1824-
// * Because `src` is a `&[T]` and dst is a `&[T]` within the `Vec<T>`,
1825-
// `copy_nonoverlapping`'s alignment requirements are met.
1826-
// * Source and destination ranges cannot overlap as we just reserved the destination
1850+
// * Source and destination data ranges cannot overlap as we just reserved the destination
18271851
// range from the bump.
18281852
unsafe {
1829-
let src = other.as_ptr();
1830-
let dst = self.as_mut_ptr().add(old_len);
1831-
ptr::copy_nonoverlapping(src, dst, other.len());
1853+
self.extend_from_slice_copy_unchecked(other);
18321854
}
1855+
}
1856+
1857+
/// For each slice in `slices`, copies all elements in the slice and appends them to the `Vec`.
1858+
///
1859+
/// This method is equivalent to calling [`extend_from_slice_copy`] in a loop, but is able
1860+
/// to precompute the total amount of space to reserve in advance. This reduces the potential
1861+
/// maximum number of reallocations needed from one-per-slice to just one.
1862+
///
1863+
/// # Examples
1864+
///
1865+
/// ```
1866+
/// use bumpalo::{Bump, collections::Vec};
1867+
///
1868+
/// let b = Bump::new();
1869+
///
1870+
/// let mut vec = bumpalo::vec![in &b; 1];
1871+
/// vec.extend_from_slices_copy(&[&[2, 3], &[], &[4]]);
1872+
/// assert_eq!(vec, [1, 2, 3, 4]);
1873+
/// ```
1874+
///
1875+
/// ```
1876+
/// use bumpalo::{Bump, collections::Vec};
1877+
///
1878+
/// let b = Bump::new();
1879+
///
1880+
/// let mut vec = bumpalo::vec![in &b; 'H' as u8];
1881+
/// vec.extend_from_slices_copy(&["ello,".as_bytes(), &[], " world!".as_bytes()]);
1882+
/// assert_eq!(vec, "Hello, world!".as_bytes());
1883+
/// ```
1884+
///
1885+
/// [`extend_from_slice_copy`]: #method.extend_from_slice_copy
1886+
pub fn extend_from_slices_copy(&mut self, slices: &[&[T]]) {
1887+
// Reserve the total amount of capacity we'll need to safely append the aggregated contents
1888+
// of each slice in `slices`.
1889+
let capacity_to_reserve: usize = slices.iter().map(|slice| slice.len()).sum();
1890+
self.reserve(capacity_to_reserve);
18331891

1834-
// Update length of Vec to include values just pushed
1835-
// SAFETY: We reserved sufficient capacity for the values above.
1836-
// The elements at `old_len..new_len` were initialized by `copy_nonoverlapping` above.
1837-
unsafe { self.set_len(new_len) };
1892+
// SAFETY:
1893+
// * `dst` is valid for writes of `capacity_to_reserve` items as
1894+
// `self.reserve(capacity_to_reserve)` above guarantees that.
1895+
// * Source and destination ranges cannot overlap as we just reserved the destination
1896+
// range from the bump.
1897+
unsafe {
1898+
// Copy the contents of each slice onto the end of `self`
1899+
slices.iter().for_each(|slice| {
1900+
self.extend_from_slice_copy_unchecked(slice);
1901+
});
1902+
}
18381903
}
18391904
}
18401905

tests/all/quickchecks.rs

+86
Original file line numberDiff line numberDiff line change
@@ -306,4 +306,90 @@ quickcheck! {
306306
}
307307
}
308308
}
309+
310+
#[cfg(feature = "collections")]
311+
fn extending_from_slice(data1: Vec<usize>, data2: Vec<usize>) -> () {
312+
let bump = Bump::new();
313+
314+
// Create a bumpalo Vec with the contents of `data1`
315+
let mut vec = bumpalo::collections::Vec::new_in(&bump);
316+
vec.extend_from_slice_copy(&data1);
317+
assert_eq!(vec.as_slice(), data1);
318+
319+
// Extend the Vec using the contents of `data2`
320+
vec.extend_from_slice_copy(&data2);
321+
// Confirm that the Vec now has the expected number of items
322+
assert_eq!(vec.len(), data1.len() + data2.len());
323+
// Confirm that the beginning of the Vec matches `data1`'s elements
324+
assert_eq!(&vec[0..data1.len()], data1);
325+
// Confirm that the end of the Vec matches `data2`'s elements
326+
assert_eq!(&vec[data1.len()..], data2);
327+
}
328+
329+
#[cfg(feature = "collections")]
330+
fn extending_from_slices(data: Vec<Vec<usize>>) -> () {
331+
let bump = Bump::new();
332+
333+
// Convert the Vec<Vec<usize>> into a &[&[usize]]
334+
let slices_vec: Vec<&[usize]> = data.iter().map(Vec::as_slice).collect();
335+
let slices = slices_vec.as_slice();
336+
337+
// Isolate the first slice from the remaining slices. If `slices` is empty,
338+
// fall back to empty slices for both.
339+
let (first_slice, remaining_slices) = match slices {
340+
[head, tail @ ..] => (*head, tail),
341+
[] => (&[][..], &[][..])
342+
};
343+
344+
// Create a bumpalo `Vec` and populate it with the contents of the first slice.
345+
let mut vec = bumpalo::collections::Vec::new_in(&bump);
346+
vec.extend_from_slice_copy(first_slice);
347+
assert_eq!(vec.as_slice(), first_slice);
348+
349+
// Append all of the other slices onto the end of the Vec
350+
vec.extend_from_slices_copy(remaining_slices);
351+
352+
let total_length: usize = slices.iter().map(|s| s.len()).sum();
353+
assert_eq!(vec.len(), total_length);
354+
355+
let total_data: Vec<usize> = slices.iter().flat_map(|s| s.iter().copied()).collect();
356+
assert_eq!(vec.as_slice(), total_data.as_slice());
357+
}
358+
359+
#[cfg(feature = "collections")]
360+
fn compare_extending_from_slice_and_from_slices(data: Vec<Vec<usize>>) -> () {
361+
let bump = Bump::new();
362+
363+
// Convert the Vec<Vec<usize>> into a &[&[usize]]
364+
let slices_vec: Vec<&[usize]> = data.iter().map(Vec::as_slice).collect();
365+
let slices = slices_vec.as_slice();
366+
367+
// Isolate the first slice from the remaining slices. If `slices` is empty,
368+
// fall back to empty slices for both.
369+
let (first_slice, remaining_slices) = match slices {
370+
[head, tail @ ..] => (*head, tail),
371+
[] => (&[][..], &[][..])
372+
};
373+
374+
// Create a bumpalo `Vec` and populate it with the contents of the first slice.
375+
let mut vec1 = bumpalo::collections::Vec::new_in(&bump);
376+
vec1.extend_from_slice_copy(first_slice);
377+
assert_eq!(vec1.as_slice(), first_slice);
378+
379+
// Append each remaining slice individually
380+
for slice in remaining_slices {
381+
vec1.extend_from_slice_copy(slice);
382+
}
383+
384+
// Create a second Vec populated with the contents of the first slice.
385+
let mut vec2 = bumpalo::collections::Vec::new_in(&bump);
386+
vec2.extend_from_slice_copy(first_slice);
387+
assert_eq!(vec2.as_slice(), first_slice);
388+
389+
// Append the remaining slices en masse
390+
vec2.extend_from_slices_copy(remaining_slices);
391+
392+
// Confirm that the two approaches to extending a Vec resulted in the same data
393+
assert_eq!(vec1, vec2);
394+
}
309395
}

tests/all/vec.rs

+35
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,41 @@ fn test_vec_items_get_dropped() {
106106
assert_eq!("Dropped!Dropped!", buffer.borrow().deref());
107107
}
108108

109+
#[test]
110+
fn test_extend_from_slice_copy() {
111+
let bump = Bump::new();
112+
let mut vec = vec![in &bump; 1, 2, 3];
113+
assert_eq!(&[1, 2, 3][..], vec.as_slice());
114+
115+
vec.extend_from_slice_copy(&[4, 5, 6]);
116+
assert_eq!(&[1, 2, 3, 4, 5, 6][..], vec.as_slice());
117+
118+
// Confirm that passing an empty slice is a no-op
119+
vec.extend_from_slice_copy(&[]);
120+
assert_eq!(&[1, 2, 3, 4, 5, 6][..], vec.as_slice());
121+
122+
vec.extend_from_slice_copy(&[7]);
123+
assert_eq!(&[1, 2, 3, 4, 5, 6, 7][..], vec.as_slice());
124+
}
125+
126+
#[test]
127+
fn test_extend_from_slices_copy() {
128+
let bump = Bump::new();
129+
let mut vec = vec![in &bump; 1, 2, 3];
130+
assert_eq!(&[1, 2, 3][..], vec.as_slice());
131+
132+
// Confirm that passing an empty slice of slices is a no-op
133+
vec.extend_from_slices_copy(&[]);
134+
assert_eq!(&[1, 2, 3][..], vec.as_slice());
135+
136+
// Confirm that an empty slice in the slice-of-slices is a no-op
137+
vec.extend_from_slices_copy(&[&[4, 5, 6], &[], &[7]]);
138+
assert_eq!(&[1, 2, 3, 4, 5, 6, 7][..], vec.as_slice());
139+
140+
vec.extend_from_slices_copy(&[&[8], &[9, 10, 11], &[12]]);
141+
assert_eq!(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], vec.as_slice());
142+
}
143+
109144
#[cfg(feature = "std")]
110145
#[test]
111146
fn test_vec_write() {

0 commit comments

Comments
 (0)