optimize joining and concatenation for slices
for both Vec<T> and String - eliminates the boolean first flag in fn join() for String only - eliminates repeated bounds checks in join(), concat() - adds fast paths for small string separators up to a len of 4 bytes
This commit is contained in:
parent
577a5b2703
commit
d866082050
@ -566,18 +566,18 @@ impl<T: Clone, V: Borrow<[T]>> SliceConcatExt<T> for [V] {
|
||||
}
|
||||
|
||||
fn join(&self, sep: &T) -> Vec<T> {
|
||||
let size = self.iter().fold(0, |acc, v| acc + v.borrow().len());
|
||||
let mut result = Vec::with_capacity(size + self.len());
|
||||
let mut first = true;
|
||||
for v in self {
|
||||
if first {
|
||||
first = false
|
||||
} else {
|
||||
result.push(sep.clone())
|
||||
let mut iter = self.iter();
|
||||
iter.next().map_or(vec![], |first| {
|
||||
let size = self.iter().fold(0, |acc, v| acc + v.borrow().len());
|
||||
let mut result = Vec::with_capacity(size + self.len());
|
||||
result.extend_from_slice(first.borrow());
|
||||
|
||||
for v in iter {
|
||||
result.push(sep.clone());
|
||||
result.extend_from_slice(v.borrow())
|
||||
}
|
||||
result.extend_from_slice(v.borrow())
|
||||
}
|
||||
result
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
fn connect(&self, sep: &T) -> Vec<T> {
|
||||
|
@ -86,47 +86,13 @@ impl<S: Borrow<str>> SliceConcatExt<str> for [S] {
|
||||
type Output = String;
|
||||
|
||||
fn concat(&self) -> String {
|
||||
if self.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
// `len` calculation may overflow but push_str will check boundaries
|
||||
let len = self.iter().map(|s| s.borrow().len()).sum();
|
||||
let mut result = String::with_capacity(len);
|
||||
|
||||
for s in self {
|
||||
result.push_str(s.borrow())
|
||||
}
|
||||
|
||||
result
|
||||
self.join("")
|
||||
}
|
||||
|
||||
fn join(&self, sep: &str) -> String {
|
||||
if self.is_empty() {
|
||||
return String::new();
|
||||
unsafe {
|
||||
String::from_utf8_unchecked( join_generic_copy(self, sep.as_bytes()) )
|
||||
}
|
||||
|
||||
// concat is faster
|
||||
if sep.is_empty() {
|
||||
return self.concat();
|
||||
}
|
||||
|
||||
// this is wrong without the guarantee that `self` is non-empty
|
||||
// `len` calculation may overflow but push_str but will check boundaries
|
||||
let len = sep.len() * (self.len() - 1) +
|
||||
self.iter().map(|s| s.borrow().len()).sum::<usize>();
|
||||
let mut result = String::with_capacity(len);
|
||||
let mut first = true;
|
||||
|
||||
for s in self {
|
||||
if first {
|
||||
first = false;
|
||||
} else {
|
||||
result.push_str(sep);
|
||||
}
|
||||
result.push_str(s.borrow());
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn connect(&self, sep: &str) -> String {
|
||||
@ -134,6 +100,106 @@ impl<S: Borrow<str>> SliceConcatExt<str> for [S] {
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! spezialize_for_lengths {
|
||||
($separator:expr, $target:expr, $iter:expr; $($num:expr),*) => {
|
||||
let mut target = $target;
|
||||
let iter = $iter;
|
||||
let sep_len = $separator.len();
|
||||
let sep_bytes = $separator;
|
||||
match $separator.len() {
|
||||
$(
|
||||
// loops with hardcoded sizes run much faster
|
||||
// specialize the cases with small separator lengths
|
||||
$num => {
|
||||
for s in iter {
|
||||
target.get_unchecked_mut(..$num)
|
||||
.copy_from_slice(sep_bytes);
|
||||
|
||||
let s_bytes = s.borrow().as_ref();
|
||||
let offset = s_bytes.len();
|
||||
target = {target}.get_unchecked_mut($num..);
|
||||
target.get_unchecked_mut(..offset)
|
||||
.copy_from_slice(s_bytes);
|
||||
target = {target}.get_unchecked_mut(offset..);
|
||||
}
|
||||
},
|
||||
)*
|
||||
0 => {
|
||||
// concat, same principle without the separator
|
||||
for s in iter {
|
||||
let s_bytes = s.borrow().as_ref();
|
||||
let offset = s_bytes.len();
|
||||
target.get_unchecked_mut(..offset)
|
||||
.copy_from_slice(s_bytes);
|
||||
target = {target}.get_unchecked_mut(offset..);
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
// arbitrary non-zero size fallback
|
||||
for s in iter {
|
||||
target.get_unchecked_mut(..sep_len)
|
||||
.copy_from_slice(sep_bytes);
|
||||
|
||||
let s_bytes = s.borrow().as_ref();
|
||||
let offset = s_bytes.len();
|
||||
target = {target}.get_unchecked_mut(sep_len..);
|
||||
target.get_unchecked_mut(..offset)
|
||||
.copy_from_slice(s_bytes);
|
||||
target = {target}.get_unchecked_mut(offset..);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Optimized join implementation that works for both Vec<T> (T: Copy) and String's inner vec
|
||||
// Currently (2018-05-13) there is a bug with type inference and specialization (see issue #36262)
|
||||
// For this reason SliceConcatExt<T> is not specialized for T: Copy and SliceConcatExt<str> is the
|
||||
// only user of this function. It is left in place for the time when that is fixed.
|
||||
//
|
||||
// the bounds for String-join are S: Borrow<str> and for Vec-join Borrow<[T]>
|
||||
// [T] and str both impl AsRef<[T]> for some T
|
||||
// => s.borrow().as_ref() and we always have slices
|
||||
fn join_generic_copy<B, T, S>(slice: &[S], sep: &[T]) -> Vec<T>
|
||||
where
|
||||
T: Copy,
|
||||
B: AsRef<[T]> + ?Sized,
|
||||
S: Borrow<B>,
|
||||
{
|
||||
let sep_len = sep.len();
|
||||
let mut iter = slice.iter();
|
||||
iter.next().map_or(vec![], |first| {
|
||||
// this is wrong without the guarantee that `slice` is non-empty
|
||||
// if the `len` calculation overflows, we'll panic
|
||||
// we would have run out of memory anyway and the rest of the function requires
|
||||
// the entire String pre-allocated for safety
|
||||
//
|
||||
// this is the exact len of the resulting String
|
||||
let len = sep_len.checked_mul(slice.len() - 1).and_then(|n| {
|
||||
slice.iter().map(|s| s.borrow().as_ref().len()).try_fold(n, usize::checked_add)
|
||||
}).expect("attempt to join into collection with len > usize::MAX");
|
||||
|
||||
// crucial for safety
|
||||
let mut result = Vec::with_capacity(len);
|
||||
|
||||
unsafe {
|
||||
result.extend_from_slice(first.borrow().as_ref());
|
||||
|
||||
{
|
||||
let pos = result.len();
|
||||
let target = result.get_unchecked_mut(pos..len);
|
||||
|
||||
// copy separator and strs over without bounds checks
|
||||
// generate loops with hardcoded offsets for small separators
|
||||
// massive improvements possible (~ x2)
|
||||
spezialize_for_lengths!(sep, target, iter; 1, 2, 3, 4);
|
||||
}
|
||||
result.set_len(len);
|
||||
}
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
#[stable(feature = "rust1", since = "1.0.0")]
|
||||
impl Borrow<str> for String {
|
||||
#[inline]
|
||||
|
Loading…
Reference in New Issue
Block a user