Skip to content

Commit f676ddd

Browse files
committed
optimize joining and concatenation for slices
for both Vec<T> and String - eliminates the boolean first flag in fn join() - eliminates repeated bounds checks in join(), concat() (only for T: Copy and for Strings) - adds fast paths for small string separators up to a len of 4 bytes
1 parent 9ff8ec8 commit f676ddd

File tree

2 files changed

+126
-51
lines changed

2 files changed

+126
-51
lines changed

src/liballoc/slice.rs

Lines changed: 122 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -565,7 +565,7 @@ pub trait SliceConcatExt<T: ?Sized> {
565565
impl<T: Clone, V: Borrow<[T]>> SliceConcatExt<T> for [V] {
566566
type Output = Vec<T>;
567567

568-
fn concat(&self) -> Vec<T> {
568+
default fn concat(&self) -> Vec<T> {
569569
let size = self.iter().fold(0, |acc, v| acc + v.borrow().len());
570570
let mut result = Vec::with_capacity(size);
571571
for v in self {
@@ -574,26 +574,135 @@ impl<T: Clone, V: Borrow<[T]>> SliceConcatExt<T> for [V] {
574574
result
575575
}
576576

577-
fn join(&self, sep: &T) -> Vec<T> {
578-
let size = self.iter().fold(0, |acc, v| acc + v.borrow().len());
579-
let mut result = Vec::with_capacity(size + self.len());
580-
let mut first = true;
581-
for v in self {
582-
if first {
583-
first = false
584-
} else {
585-
result.push(sep.clone())
577+
default fn join(&self, sep: &T) -> Vec<T> {
578+
let mut iter = self.iter();
579+
iter.next().map_or(vec![], |first| {
580+
let size = self.iter().fold(0, |acc, v| acc + v.borrow().len());
581+
let mut result = Vec::with_capacity(size + self.len());
582+
result.extend_from_slice(first.borrow());
583+
584+
for v in iter {
585+
result.push(sep.clone());
586+
result.extend_from_slice(v.borrow())
586587
}
587-
result.extend_from_slice(v.borrow())
588-
}
589-
result
588+
result
589+
})
590590
}
591591

592592
fn connect(&self, sep: &T) -> Vec<T> {
593593
self.join(sep)
594594
}
595595
}
596596

597+
#[unstable(feature = "slice_concat_ext",
598+
reason = "trait should not have to exist",
599+
issue = "27747")]
600+
impl<T: Copy, V: Borrow<[T]>> SliceConcatExt<T> for [V] {
601+
fn concat(&self) -> Vec<T> {
602+
join_generic_copy(self, &[])
603+
}
604+
605+
fn join(&self, sep: &T) -> Vec<T> {
606+
join_generic_copy(self, &[*sep])
607+
}
608+
}
609+
610+
macro_rules! spezialize_for_lengths {
611+
($separator:expr, $target:expr, $iter:expr; $($num:expr),*) => {
612+
let mut target = $target;
613+
let iter = $iter;
614+
let sep_len = $separator.len();
615+
let sep_bytes = $separator;
616+
match $separator.len() {
617+
$(
618+
// loops with hardcoded sizes run much faster
619+
// specialize the cases with small separator lengths
620+
$num => {
621+
for s in iter {
622+
target.get_unchecked_mut(..$num)
623+
.copy_from_slice(sep_bytes);
624+
625+
let s_bytes = s.borrow().as_ref();
626+
let offset = s_bytes.len();
627+
target = {target}.get_unchecked_mut($num..);
628+
target.get_unchecked_mut(..offset)
629+
.copy_from_slice(s_bytes);
630+
target = {target}.get_unchecked_mut(offset..);
631+
}
632+
},
633+
)*
634+
0 => {
635+
// concat, same principle without the separator
636+
for s in iter {
637+
let s_bytes = s.borrow().as_ref();
638+
let offset = s_bytes.len();
639+
target.get_unchecked_mut(..offset)
640+
.copy_from_slice(s_bytes);
641+
target = {target}.get_unchecked_mut(offset..);
642+
}
643+
},
644+
_ => {
645+
// arbitrary non-zero size fallback
646+
for s in iter {
647+
target.get_unchecked_mut(..sep_len)
648+
.copy_from_slice(sep_bytes);
649+
650+
let s_bytes = s.borrow().as_ref();
651+
let offset = s_bytes.len();
652+
target = {target}.get_unchecked_mut(sep_len..);
653+
target.get_unchecked_mut(..offset)
654+
.copy_from_slice(s_bytes);
655+
target = {target}.get_unchecked_mut(offset..);
656+
}
657+
}
658+
}
659+
};
660+
}
661+
662+
// Optimized join implementation that works for both Vec<T> and String's inner vec
663+
// the bounds for String-join are S: Borrow<str> and for Vec-join Borrow<[T]>
664+
// [T] and str both impl AsRef<[T]> for some T
665+
// => s.borrow().as_ref() and we always have slices
666+
pub(crate) fn join_generic_copy<B, T, S>(slice: &[S], sep: &[T]) -> Vec<T>
667+
where
668+
T: Copy,
669+
B: AsRef<[T]> + ?Sized,
670+
S: Borrow<B>,
671+
{
672+
let sep_len = sep.len();
673+
let mut iter = slice.iter();
674+
iter.next().map_or(vec![], |first| {
675+
// this is wrong without the guarantee that `slice` is non-empty
676+
// if the `len` calculation overflows, we'll panic
677+
// we would have run out of memory anyway and the rest of the function requires
678+
// the entire String pre-allocated for safety
679+
//
680+
// this is the exact len of the resulting String
681+
let len = sep_len.checked_mul(slice.len() - 1).and_then(|n| {
682+
slice.iter().map(|s| s.borrow().as_ref().len()).try_fold(n, usize::checked_add)
683+
}).expect("attempt to join into collection with len > usize::MAX");
684+
685+
// crucial for safety
686+
let mut result = Vec::with_capacity(len);
687+
688+
unsafe {
689+
result.extend_from_slice(first.borrow().as_ref());
690+
691+
{
692+
let pos = result.len();
693+
let target = result.get_unchecked_mut(pos..len);
694+
695+
// copy separator and strs over without bounds checks
696+
// generate loops with hardcoded offsets for small separators
697+
// massive improvements possible (~ x2)
698+
spezialize_for_lengths!(sep, target, iter; 1, 2, 3, 4);
699+
}
700+
result.set_len(len);
701+
}
702+
result
703+
})
704+
}
705+
597706
////////////////////////////////////////////////////////////////////////////////
598707
// Standard trait implementations for slices
599708
////////////////////////////////////////////////////////////////////////////////

src/liballoc/str.rs

Lines changed: 4 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ use core::iter::FusedIterator;
4949

5050
use borrow::{Borrow, ToOwned};
5151
use boxed::Box;
52-
use slice::{SliceConcatExt, SliceIndex};
52+
use slice::{SliceConcatExt, SliceIndex, join_generic_copy};
5353
use string::String;
5454
use vec::Vec;
5555
use vec_deque::VecDeque;
@@ -87,47 +87,13 @@ impl<S: Borrow<str>> SliceConcatExt<str> for [S] {
8787
type Output = String;
8888

8989
fn concat(&self) -> String {
90-
if self.is_empty() {
91-
return String::new();
92-
}
93-
94-
// `len` calculation may overflow but push_str will check boundaries
95-
let len = self.iter().map(|s| s.borrow().len()).sum();
96-
let mut result = String::with_capacity(len);
97-
98-
for s in self {
99-
result.push_str(s.borrow())
100-
}
101-
102-
result
90+
self.join("")
10391
}
10492

10593
fn join(&self, sep: &str) -> String {
106-
if self.is_empty() {
107-
return String::new();
108-
}
109-
110-
// concat is faster
111-
if sep.is_empty() {
112-
return self.concat();
94+
unsafe {
95+
String::from_utf8_unchecked( join_generic_copy(self, sep.as_bytes()) )
11396
}
114-
115-
// this is wrong without the guarantee that `self` is non-empty
116-
// `len` calculation may overflow but push_str but will check boundaries
117-
let len = sep.len() * (self.len() - 1) +
118-
self.iter().map(|s| s.borrow().len()).sum::<usize>();
119-
let mut result = String::with_capacity(len);
120-
let mut first = true;
121-
122-
for s in self {
123-
if first {
124-
first = false;
125-
} else {
126-
result.push_str(sep);
127-
}
128-
result.push_str(s.borrow());
129-
}
130-
result
13197
}
13298

13399
fn connect(&self, sep: &str) -> String {

0 commit comments

Comments
 (0)