Skip to content

Commit

Permalink
Merge pull request #229 from overlookmotel/faster-push-str
Browse files Browse the repository at this point in the history
Improve performance of `String::push_str`
  • Loading branch information
fitzgen committed Feb 14, 2024
2 parents 285d969 + 0cb5a75 commit 1d31c76
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 2 deletions.
26 changes: 25 additions & 1 deletion benches/benches.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,14 @@ fn format_realloc(bump: &bumpalo::Bump, n: usize) {
criterion::black_box(s);
}

#[cfg(feature = "collections")]
fn string_push_str(bump: &bumpalo::Bump, str: &str) {
let str = criterion::black_box(str);
let mut s = bumpalo::collections::string::String::with_capacity_in(str.len(), bump);
s.push_str(str);
criterion::black_box(s);
}

const ALLOCATIONS: usize = 10_000;

fn bench_alloc(c: &mut Criterion) {
Expand Down Expand Up @@ -202,6 +210,21 @@ fn bench_format_realloc(c: &mut Criterion) {
}
}

fn bench_string_push_str(c: &mut Criterion) {
let len: usize = 16 * 1024; // 16 KiB

let mut group = c.benchmark_group("alloc");
group.throughput(Throughput::Elements(len as u64));
group.bench_function("push_str", |b| {
let mut bump = bumpalo::Bump::with_capacity(len);
let str = "x".repeat(len);
b.iter(|| {
bump.reset();
string_push_str(&bump, &*str);
});
});
}

criterion_group!(
benches,
bench_alloc,
Expand All @@ -212,6 +235,7 @@ criterion_group!(
bench_try_alloc_with,
bench_try_alloc_try_with,
bench_try_alloc_try_with_err,
bench_format_realloc
bench_format_realloc,
bench_string_push_str
);
criterion_main!(benches);
26 changes: 25 additions & 1 deletion src/collections/string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -925,7 +925,31 @@ impl<'bump> String<'bump> {
/// ```
#[inline]
pub fn push_str(&mut self, string: &str) {
self.vec.extend_from_slice(string.as_bytes())
// Reserve space in the Vec for the string to be added
let old_len = self.vec.len();
self.vec.reserve(string.len());

let new_len = old_len + string.len();
debug_assert!(new_len <= self.vec.capacity());

// Copy string into space just reserved
// SAFETY:
// * `src` is valid for reads of `string.len()` bytes by virtue of being an allocated `&str`.
// * `dst` is valid for writes of `string.len()` bytes as `self.vec.reserve(string.len())`
// above guarantees that.
// * Alignment is not relevant as `u8` has no alignment requirements.
// * Source and destination ranges cannot overlap as we just reserved the destination
// range from the bump.
unsafe {
let src = string.as_ptr();
let dst = self.vec.as_mut_ptr().add(old_len);
ptr::copy_nonoverlapping(src, dst, string.len());
}

// Update length of Vec to include string just pushed
// SAFETY: We reserved sufficent capacity for the string above.
// The elements at `old_len..new_len` were initialized by `copy_nonoverlapping` above.
unsafe { self.vec.set_len(new_len) };
}

/// Returns this `String`'s capacity, in bytes.
Expand Down
17 changes: 17 additions & 0 deletions tests/all/string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,20 @@ fn trailing_comma_in_format_macro() {
let v = format![in &b, "{}{}", 1, 2, ];
assert_eq!(v, "12");
}

#[test]
fn push_str() {
let b = Bump::new();
let mut s = String::new_in(&b);
s.push_str("abc");
assert_eq!(s, "abc");
s.push_str("def");
assert_eq!(s, "abcdef");
s.push_str("");
assert_eq!(s, "abcdef");
s.push_str(&"x".repeat(4000));
assert_eq!(s.len(), 4006);
s.push_str("ghi");
assert_eq!(s.len(), 4009);
assert_eq!(&s[s.len() - 5..], "xxghi");
}

0 comments on commit 1d31c76

Please sign in to comment.