Skip to content

Commit 78bc491

Browse files
committed
Replace extend_from_slice with a verified, optimized implementation
Replaced the existing extend_from_slice with the implementation used in my formally verified bump allocator. Replaced verus proofs with comments. Additionally, lifted the write + length increment in push to push_unchecked, without exposing this publicly. In the benchmark 'extend 16384 bytes/extend_from_slice', throughput improved +13361%. I am dubious the benchmark is being compiled out due to the monotone increase with respect to the input size, as well as the correspondence with 'extend_from_slice_copy' benchmarks. No changes in public API, all tests pass (release, debug, miri), and MSRV remains 1.71.1.
1 parent e5c128d commit 78bc491

File tree

4 files changed

+179
-17
lines changed

4 files changed

+179
-17
lines changed

benches/benches.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -196,9 +196,9 @@ fn bench_extend_from_slices_copy(c: &mut Criterion) {
196196
for is_preallocated in is_preallocated_settings {
197197
for num_slices in slice_counts.iter().copied() {
198198
// 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-
);
199+
let mut group = c.benchmark_group(format!(
200+
"extend_from_slices num_slices={num_slices}, is_preallocated={is_preallocated}"
201+
));
202202

203203
// Cycle over `data` to construct a slice of slices to append
204204
let slices = data
@@ -223,7 +223,8 @@ fn bench_extend_from_slices_copy(c: &mut Criterion) {
223223
group.bench_function("loop over extend_from_slice_copy", |b| {
224224
b.iter(|| {
225225
bump.reset();
226-
let mut vec = bumpalo::collections::Vec::<u8>::with_capacity_in(size_to_allocate, &bump);
226+
let mut vec =
227+
bumpalo::collections::Vec::<u8>::with_capacity_in(size_to_allocate, &bump);
227228
for slice in black_box(&slices) {
228229
vec.extend_from_slice_copy(slice);
229230
}
@@ -237,7 +238,8 @@ fn bench_extend_from_slices_copy(c: &mut Criterion) {
237238
group.bench_function("extend_from_slices_copy", |b| {
238239
b.iter(|| {
239240
bump.reset();
240-
let mut vec = bumpalo::collections::Vec::<u8>::with_capacity_in(size_to_allocate, &bump);
241+
let mut vec =
242+
bumpalo::collections::Vec::<u8>::with_capacity_in(size_to_allocate, &bump);
241243
vec.extend_from_slices_copy(black_box(slices.as_slice()));
242244
black_box(vec.as_slice());
243245
});

src/collections/raw_vec.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -639,10 +639,11 @@ impl<'a, T> RawVec<'a, T> {
639639
#[inline(never)]
640640
fn reserve_internal_or_error(
641641
&mut self,
642-
used_cap: usize,
643-
needed_extra_cap: usize,
644-
fallibility: Fallibility,
645-
strategy: ReserveStrategy,)-> Result<(), CollectionAllocErr> {
642+
used_cap: usize,
643+
needed_extra_cap: usize,
644+
fallibility: Fallibility,
645+
strategy: ReserveStrategy,
646+
) -> Result<(), CollectionAllocErr> {
646647
// Delegates the call to `reserve_internal`, which can be inlined.
647648
self.reserve_internal(used_cap, needed_extra_cap, fallibility, strategy)
648649
}

src/collections/vec.rs

Lines changed: 166 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
88
// option. This file may not be copied, modified, or distributed
99
// except according to those terms.
10-
1110
//! A contiguous growable array type with heap-allocated contents, written
1211
//! [`Vec<'bump, T>`].
1312
//!
@@ -1404,6 +1403,33 @@ impl<'bump, T: 'bump> Vec<'bump, T> {
14041403
self.truncate(len);
14051404
}
14061405

1406+
// Proven specification with verus, converted to comments.
1407+
/// # Preconditions
1408+
///
1409+
/// - old(self).len() < old(self).capacity(),
1410+
///
1411+
/// # Postconditions
1412+
///
1413+
/// - self.get_unchecked(old(self).len()) == value,
1414+
/// - self.len() == old(self).len() + 1,
1415+
/// - self.capacity() == old(self).capacity(),
1416+
/// - forall|i: usize| implies(
1417+
/// i < old(self).len(),
1418+
/// self.get_unchecked(i) == old(self).get_unchecked(i)
1419+
/// )
1420+
#[allow(clippy::inline_always)]
1421+
#[inline(always)]
1422+
unsafe fn push_unchecked(&mut self, value: T) {
1423+
debug_assert!(self.len() < self.capacity());
1424+
1425+
// Divergence from verified impl:
1426+
// Verified implementation has special handling for ZSTs
1427+
// as ZSTs do not play nicely with separation logic.
1428+
ptr::write(self.buf.ptr().add(self.len), value);
1429+
1430+
self.len = self.len + 1;
1431+
}
1432+
14071433
/// Appends an element to the back of a vector.
14081434
///
14091435
/// # Panics
@@ -1429,9 +1455,7 @@ impl<'bump, T: 'bump> Vec<'bump, T> {
14291455
self.reserve(1);
14301456
}
14311457
unsafe {
1432-
let end = self.buf.ptr().add(self.len);
1433-
ptr::write(end, value);
1434-
self.len += 1;
1458+
self.push_unchecked(value);
14351459
}
14361460
}
14371461

@@ -1749,6 +1773,108 @@ impl<'bump, T: 'bump + Clone> Vec<'bump, T> {
17491773
}
17501774
}
17511775

1776+
// Proven specification with verus, converted to comments.
1777+
/// # Preconditions
1778+
///
1779+
/// - old(self).len() + slice.len() <= old(self).capacity(),
1780+
///
1781+
/// # Postconditions
1782+
///
1783+
/// - forall|i: usize| implies(
1784+
/// i < old(self).len(),
1785+
/// self.get_unchecked(i) == old(self).get_unchecked(i)
1786+
/// ),
1787+
/// - forall|i: usize| implies(
1788+
/// i < slice.len(),
1789+
/// self.get_unchecked((old(self).len() + i) as usize)
1790+
/// == clone(slice.get_unchecked(i))
1791+
/// ),
1792+
/// - self.len() == old(self).len() + slice.len(),
1793+
/// - self.capacity() == old(self).capacity(),
1794+
#[inline]
1795+
unsafe fn extend_from_slice_unchecked(&mut self, slice: &[T]) {
1796+
// Guaranteed never to overflow for non ZSTs
1797+
// size_of::<T>() <= isize::MAX - (isize::MAX % align_of::<T>()))
1798+
// isize::MAX + isize::MAX < usize::MAX
1799+
debug_assert!(
1800+
core::mem::size_of::<T>() == 0 || self.capacity() >= self.len() + slice.len()
1801+
);
1802+
debug_assert!(
1803+
// is_zst::<T>() ==> capacity >= slen + slice.len()
1804+
core::mem::size_of::<T>() != 0
1805+
// Capacity is usize::MAX for ZSTs
1806+
|| self.len() <= usize::MAX - slice.len()
1807+
);
1808+
1809+
let mut pos = 0usize;
1810+
1811+
loop
1812+
/*
1813+
invariants
1814+
pos <= slice.len(),
1815+
self.len() + (slice.len() - pos) <= old(self).capacity(),
1816+
old(self).capacity() == self.capacity(),
1817+
1818+
self.len() == old(self).len() + pos,
1819+
1820+
forall|i: usize| implies(
1821+
i < old(self).len(),
1822+
self.get_unchecked(i) == old(self).get_unchecked(i)
1823+
),
1824+
forall|i: usize| implies(
1825+
i < pos,
1826+
self.get_unchecked((old(self).len() + i) as usize)
1827+
== clone(slice.get_unchecked(i))
1828+
)
1829+
*/
1830+
{
1831+
if pos == slice.len() {
1832+
/*
1833+
pos = slice.len(),
1834+
self.len() = old(self).len() + slice.len(),
1835+
1836+
forall|i: usize| i < slice.len() implies {
1837+
self.get_unchecked((old(self).len() + i) as usize)
1838+
== clone(slice.get_unchecked(i))
1839+
}
1840+
by {
1841+
i < pos
1842+
}
1843+
*/
1844+
return;
1845+
}
1846+
1847+
/*
1848+
pos < slice.len(),
1849+
self.len() < self.capacity()
1850+
*/
1851+
1852+
let elem = slice.get_unchecked(pos);
1853+
self.push_unchecked(elem.clone());
1854+
1855+
/*
1856+
ghost prev_pos = pos
1857+
*/
1858+
1859+
pos = pos + 1;
1860+
1861+
/*
1862+
forall|i: usize| i < pos implies {
1863+
self.get_unchecked((old(self).len() + i) as usize)
1864+
== clone(slice.get_unchecked(i))
1865+
}
1866+
by {
1867+
if i < pos - 1 {
1868+
// By invariant
1869+
}
1870+
else {
1871+
i == prev_pos
1872+
}
1873+
}
1874+
*/
1875+
}
1876+
}
1877+
17521878
/// Clones and appends all elements in a slice to the `Vec`.
17531879
///
17541880
/// Iterates over the slice `other`, clones each element, and then appends
@@ -1773,7 +1899,42 @@ impl<'bump, T: 'bump + Clone> Vec<'bump, T> {
17731899
///
17741900
/// [`extend`]: #method.extend
17751901
pub fn extend_from_slice(&mut self, other: &[T]) {
1776-
self.extend(other.iter().cloned())
1902+
let capacity = self.capacity();
1903+
1904+
/*
1905+
Cannot underflow via invariant of the Vec (capacity >= length).
1906+
1907+
This also holds for ZSTs, as capacity is usize::MAX
1908+
*/
1909+
let remaining_cap = capacity - self.len;
1910+
1911+
/*
1912+
self.len() + other.len() <= self.capacity(),
1913+
<==>
1914+
other.len() <= self.capacity() - self.len()
1915+
*/
1916+
1917+
if other.len() > remaining_cap {
1918+
/*
1919+
Divergence from verified impl:
1920+
Verified implementation's reserve is not the same
1921+
as bumpalo's. Verified implementation reserves with
1922+
respect to capacity, not length. Thus this is equivalent
1923+
to the verified implementation's:
1924+
1925+
self.buf.reserve(other.len() - remaining_cap)
1926+
1927+
*/
1928+
self.reserve(other.len());
1929+
}
1930+
1931+
/*
1932+
self.capacity() >= self.len() + other.len()
1933+
*/
1934+
1935+
unsafe {
1936+
self.extend_from_slice_unchecked(other);
1937+
}
17771938
}
17781939
}
17791940

@@ -1805,7 +1966,6 @@ impl<'bump, T: 'bump + Copy> Vec<'bump, T> {
18051966
}
18061967
}
18071968

1808-
18091969
/// Copies all elements in the slice `other` and appends them to the `Vec`.
18101970
///
18111971
/// Note that this function is same as [`extend_from_slice`] except that it is optimized for
@@ -2782,5 +2942,4 @@ mod serialize {
27822942
seq.end()
27832943
}
27842944
}
2785-
27862945
}

tests/all/tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,4 +227,4 @@ fn miri_stacked_borrows_issue_247() {
227227
drop(unsafe { Box::from_raw_in(a, &bump) });
228228

229229
let _b = Box::new_in(2u16, &bump);
230-
}
230+
}

0 commit comments

Comments
 (0)