about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEmerentius <emerentius@arcor.de>2018-04-30 13:09:10 +0200
committerEmerentius <emerentius@arcor.de>2018-06-01 17:13:24 +0200
commitd86608205069aed5c78bcc38dd26bcf4213e23a0 (patch)
treea507533a3d32c660c91d1f1d263a96f006dcdfc0
parent577a5b2703d97e5408664e409f35768944360fea (diff)
downloadrust-d86608205069aed5c78bcc38dd26bcf4213e23a0.tar.gz
rust-d86608205069aed5c78bcc38dd26bcf4213e23a0.zip
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
-rw-r--r--src/liballoc/slice.rs22
-rw-r--r--src/liballoc/str.rs138
2 files changed, 113 insertions, 47 deletions
diff --git a/src/liballoc/slice.rs b/src/liballoc/slice.rs
index 161493f3892..82578c3206f 100644
--- a/src/liballoc/slice.rs
+++ b/src/liballoc/slice.rs
@@ -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> {
diff --git a/src/liballoc/str.rs b/src/liballoc/str.rs
index 823e56b64e3..45daabf86ab 100644
--- a/src/liballoc/str.rs
+++ b/src/liballoc/str.rs
@@ -86,52 +86,118 @@ 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();
-        }
+    fn connect(&self, sep: &str) -> String {
+        self.join(sep)
+    }
+}
 
-        // 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;
+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..);
+                }
+            }
+        }
+    };
+}
 
-        for s in self {
-            if first {
-                first = false;
-            } else {
-                result.push_str(sep);
+// 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.push_str(s.borrow());
+            result.set_len(len);
         }
         result
-    }
-
-    fn connect(&self, sep: &str) -> String {
-        self.join(sep)
-    }
+    })
 }
 
 #[stable(feature = "rust1", since = "1.0.0")]