Skip to content
Permalink
Browse files

Add ZipTrusted, a faster Zip iterator for trusted-length iterators

This iterator matches the performance of the best-case loop for
iterating two slices side by side, with unsafe indexing.

For the triple iterator case, it's faster than the standard .zip()
iterator but it does not quite match the unsafe code.

Benchmarks:

zip_loop: Unsafe indexing of 2 slices
zip_slices_default_zip: 2 slices with .zip()
ziptrusted: 2 slices with ZipTrusted

test zip_loop                ... bench:       589 ns/iter (+/- 29)
test zip_slices_default_zip  ... bench:       874 ns/iter (+/- 43)
test ziptrusted              ... bench:       590 ns/iter (+/- 63)

zip_loop3: Unsafe indexing of 3 slices
zip_slices_default_zip3: 3 slices with .zip().zip()
ziptrusted3: 3 slices with ZipTrusted

test zip_loop3               ... bench:       860 ns/iter (+/- 46)
test zip_slices_default_zip3 ... bench:      1158 ns/iter (+/- 17)
test ziptrusted3             ... bench:      1055 ns/iter (+/- 66)
  • Loading branch information...
root
root committed Feb 7, 2015
1 parent 538bd5b commit 7bf94f9238f75b399123f5234d8a51a11658d9b1
Showing with 328 additions and 3 deletions.
  1. +176 −0 benches/bench1.rs
  2. +1 −1 src/lib.rs
  3. +151 −2 src/ziptuple.rs
@@ -3,6 +3,8 @@ extern crate itertools;

use itertools::Stride;

use itertools::{Zip, ZipTrusted};

use std::iter::repeat;

#[bench]
@@ -40,3 +42,177 @@ fn stride_iter_rev(b: &mut test::Bencher)
test::black_box(elt);
})
}

#[derive(Copy)]
struct ZipSlices<'a, T, U>
{
t_ptr: *const T,
t_end: *const T,
u_ptr: *const U,
mark: ::std::marker::ContravariantLifetime<'a>,
}

impl<'a, T, U> ZipSlices<'a, T, U>
{
pub fn new(t: &'a [T], u: &'a [U]) -> Self
{
assert!(::std::mem::size_of::<T>() != 0);
assert!(::std::mem::size_of::<U>() != 0);
let minl = std::cmp::min(t.len(), u.len());
let end_ptr = unsafe {
t.as_ptr().offset(minl as isize)
};
ZipSlices {
t_ptr: t.as_ptr(),
t_end: end_ptr,
u_ptr: u.as_ptr(),
mark: ::std::marker::ContravariantLifetime,
}
}
}

impl<'a, T, U> Iterator for ZipSlices<'a, T, U>
{
type Item = (&'a T, &'a U);

#[inline]
fn next(&mut self) -> Option<(&'a T, &'a U)>
{
if self.t_ptr == self.t_end {
return None
}
let t_elt: &T;
let u_elt: &U;
unsafe {
t_elt = ::std::mem::transmute(self.t_ptr);
self.t_ptr = self.t_ptr.offset(1);
u_elt = ::std::mem::transmute(self.u_ptr);
self.u_ptr = self.u_ptr.offset(1);
}
Some((t_elt, u_elt))
}

#[inline]
fn size_hint(&self) -> (usize, Option<usize>)
{
let len = self.t_end as usize - self.t_ptr as usize;
(len, Some(len))
}
}

#[bench]
fn zip_slices_default_zip(b: &mut test::Bencher)
{
let xs = vec![0; 1024];
let ys = vec![0; 768];

b.iter(|| for (x, y) in xs.iter().zip(ys.iter()) {
test::black_box(x);
test::black_box(y);
})
}

#[bench]
fn zip_slices_default_zip3(b: &mut test::Bencher)
{
let xs = vec![0; 1024];
let ys = vec![0; 768];
let zs = vec![0; 766];

b.iter(|| for ((x, y), z) in xs.iter().zip(ys.iter()).zip(zs.iter()) {
test::black_box(x);
test::black_box(y);
test::black_box(z);
})
}

#[bench]
fn zip_slices_ziptuple(b: &mut test::Bencher)
{
let xs = vec![0; 1024];
let ys = vec![0; 768];

b.iter(|| for (x, y) in Zip::new((xs.iter(), ys.iter())) {
test::black_box(x);
test::black_box(y);
})
}

#[bench]
fn zipslices(b: &mut test::Bencher)
{
let xs = vec![0; 1024];
let ys = vec![0; 768];

b.iter(|| for (x, y) in ZipSlices::new(&xs, &ys) {
test::black_box(x);
test::black_box(y);
})
}

#[bench]
fn ziptrusted(b: &mut test::Bencher)
{
let xs = vec![0; 1024];
let ys = vec![0; 768];

b.iter(|| for (x, y) in ZipTrusted::new((xs.iter(), ys.iter())) {
test::black_box(x);
test::black_box(y);
})
}

#[bench]
fn ziptrusted3(b: &mut test::Bencher)
{
let xs = vec![0; 1024];
let ys = vec![0; 768];
let zs = vec![0; 766];

b.iter(|| for (x, y, z) in ZipTrusted::new((xs.iter(), ys.iter(), zs.iter())) {
test::black_box(x);
test::black_box(y);
test::black_box(z);
})
}

#[bench]
fn zip_loop(b: &mut test::Bencher)
{
let xs = vec![0; 1024];
let ys = vec![0; 768];

b.iter(|| {
let len = ::std::cmp::min(xs.len(), ys.len());
for i in 0..len {
unsafe {
let x = *xs.get_unchecked(i);
let y = *ys.get_unchecked(i);
test::black_box(x);
test::black_box(y);
}
}
})
}

#[bench]
fn zip_loop3(b: &mut test::Bencher)
{
let xs = vec![0; 1024];
let ys = vec![0; 768];
let zs = vec![0; 766];

b.iter(|| {
let len = ::std::cmp::min(xs.len(), ::std::cmp::min(ys.len(), zs.len()));
for i in 0..len {
unsafe {
let x = *xs.get_unchecked(i);
let y = *ys.get_unchecked(i);
let z = *ys.get_unchecked(i);

This comment has been minimized.

@bluss

bluss Feb 7, 2015

Owner

There's a typo here, ys → zs, but fixing that doesn't even change the runtime of the loop, so the benchmarks are the same.

test::black_box(x);
test::black_box(y);
test::black_box(z);
}
}
})
}
@@ -53,7 +53,7 @@ pub use times::Times;
pub use times::times;
pub use linspace::{linspace, Linspace};
pub use zip::{ZipLongest, EitherOrBoth};
pub use ziptuple::Zip;
pub use ziptuple::{Zip, ZipTrusted};
mod adaptors;
mod intersperse;
mod islice;
@@ -1,3 +1,5 @@
use std::slice;
use std::vec;
use std::cmp;

#[derive(Clone)]
@@ -37,7 +39,7 @@ impl<T> Zip<T> where Zip<T>: Iterator
}
}

macro_rules! impl_zip_iter(
macro_rules! impl_zip_iter {
($($B:ident),*) => (
#[allow(non_snake_case)]
impl<$($B),*> Iterator for Zip<($($B,)*)>
@@ -82,7 +84,7 @@ macro_rules! impl_zip_iter(
}
}
);
);
}

impl_zip_iter!(A);
impl_zip_iter!(A, B);
@@ -93,3 +95,150 @@ impl_zip_iter!(A, B, C, D, E, F);
impl_zip_iter!(A, B, C, D, E, F, G);
impl_zip_iter!(A, B, C, D, E, F, G, H);
impl_zip_iter!(A, B, C, D, E, F, G, H, I);


/// A **TrustedIterator** has exact size, always.
pub unsafe trait TrustedIterator : ExactSizeIterator
{
/* no methods */
}

unsafe impl TrustedIterator for ::std::ops::Range<usize> { }
unsafe impl TrustedIterator for ::std::ops::Range<u32> { }
unsafe impl TrustedIterator for ::std::ops::Range<i32> { }
unsafe impl TrustedIterator for ::std::ops::Range<u16> { }
unsafe impl TrustedIterator for ::std::ops::Range<i16> { }
unsafe impl TrustedIterator for ::std::ops::Range<u8> { }
unsafe impl TrustedIterator for ::std::ops::Range<i8> { }
unsafe impl<'a, T> TrustedIterator for slice::Iter<'a, T> { }
unsafe impl<'a, T> TrustedIterator for slice::IterMut<'a, T> { }
unsafe impl<T> TrustedIterator for vec::IntoIter<T> { }


#[derive(Clone)]
/// Create an iterator running multiple iterators in lockstep.
///
/// **ZipTrusted** is an experimental version of **Zip**, and it can only use iterators that are
/// known to provide their exact size up front. The lockstep iteration can then compile to faster
/// code, ideally not checking more than once per lap for the end of iteration.
///
/// The iterator **ZipTrusted\<(I, J, ..., M)\>** is formed from a tuple of iterators and yields elements
/// until any of the subiterators yields **None**.
///
/// Iterator element type is like **(A, B, ..., E)** where **A** to **E** are the respective
/// subiterator types.
///
/// ## Example
///
/// ```
/// use itertools::ZipTrusted;
///
/// // Iterate over three sequences side-by-side
/// let mut xs = [0, 0, 0];
/// let ys = [69, 107, 101];
///
/// for (i, a, b) in ZipTrusted::new((0i32..100, xs.iter_mut(), ys.iter())) {
/// *a = i ^ *b;
/// }
///
/// assert_eq!(xs, [69, 106, 103]);
/// ```
pub struct ZipTrusted<T> {
length: usize,
t: T
}

trait SetLength {
fn set_length(&mut self);
}

impl<T> ZipTrusted<T> where ZipTrusted<T>: SetLength
{
/// Create a new **ZipTrusted** from a tuple of iterators.
#[inline]
pub fn new(t: T) -> ZipTrusted<T>
{
let mut iter = ZipTrusted {
length: 0,
t: t,
};
iter.set_length();
iter
}
}

macro_rules! impl_zip_trusted {
($($B:ident),*) => (
#[allow(non_snake_case)]
impl<$($B),*> SetLength for ZipTrusted<($($B,)*)>
where
$(
$B: TrustedIterator,
)*
{
#[inline]
fn set_length(&mut self)
{
let len = ::std::usize::MAX;
let ($(ref $B,)*) = self.t;
$(
let (l, h) = $B.size_hint();
let len = cmp::min(len, l);
debug_assert!(Some(l) == h);
)*
self.length = len;
}
}

#[allow(non_snake_case)]
impl<$($B),*> Iterator for ZipTrusted<($($B,)*)>
where
$(
$B: TrustedIterator,
)*
{
type Item = ($(<$B as Iterator>::Item,)*);

fn next(&mut self) -> Option<<Self as Iterator>::Item>
{
let ($(ref mut $B,)*) = self.t;

if self.length == 0 {
return None
}
$(
let next_opt = $B.next();
let $B;
unsafe {
::std::intrinsics::assume(match next_opt {
None => false,
Some(_) => true,
});
$B = match next_opt {
None => return None,

This comment has been minimized.

@oli-obk

oli-obk Feb 9, 2015

Contributor

shouldn't this be unreachable!()?

This comment has been minimized.

@bluss

bluss Feb 9, 2015

Owner

there is no "should", assume will make it unreachable already. panic!() in that spot performs the same as return, but intrinsics::unreachable() does not, it's much worse. For me, there is no "should", just whatever makes it compile to optimal code.

This comment has been minimized.

@oli-obk

oli-obk Feb 9, 2015

Contributor

i didn't mean intrinsics::unreachable() but unreachable!(). big difference. The latter is just a macro calling panic! with some text (http://doc.rust-lang.org/std/macro.unreachable!.html). As long as the resulting code does the same, it should be more readable. and a return None gives the reader the wrong idea.

This comment has been minimized.

@oli-obk

oli-obk Feb 9, 2015

Contributor

also odd that intrinsics::unreachable() performs badly here...

This comment has been minimized.

@bluss

bluss Feb 9, 2015

Owner

I know the difference.

Some(elt) => elt
};
}
)*
self.length -= 1;
Some(($($B,)*))
}

fn size_hint(&self) -> (usize, Option<usize>)
{
(self.length, Some(self.length))
}
}
);
}

impl_zip_trusted!(A);
impl_zip_trusted!(A, B);
impl_zip_trusted!(A, B, C);
impl_zip_trusted!(A, B, C, D);
impl_zip_trusted!(A, B, C, D, E);
impl_zip_trusted!(A, B, C, D, E, F);
impl_zip_trusted!(A, B, C, D, E, F, G);
impl_zip_trusted!(A, B, C, D, E, F, G, H);
impl_zip_trusted!(A, B, C, D, E, F, G, H, I);

0 comments on commit 7bf94f9

Please sign in to comment.
You can’t perform that action at this time.