Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Redesign the std::iter::Step trait, tweak related iterator impls for ranges #43127

Closed
wants to merge 11 commits into from

Conversation

Projects
None yet
10 participants
@SimonSapin
Copy link
Contributor

SimonSapin commented Jul 8, 2017

CC #42168

The trait is now:

/// Objects that have a notion of *successor* and *predecessor*
/// for the purpose of range iterators.
///
/// This trait is `unsafe` because implementations of the `unsafe` trait `TrustedLen`
/// depend on its implementations being correct.
#[unstable(feature = "step_trait",
           reason = "recently redesigned",
           issue = "42168")]
pub unsafe trait Step: Clone + PartialOrd + Sized {
    /// Returns the number of *successor* steps needed to get from `start` to `end`.
    ///
    /// Returns `None` if that number would overflow `usize`
    /// (or is infinite, if `end` would never be reached).
    ///
    /// This must hold for any `a`, `b`, and `n`:
    ///
    /// * `steps_between(&a, &b) == Some(0)` if and only if `a >= b`.
    /// * `steps_between(&a, &b) == Some(n)` if and only if `a.forward(n) == Some(b)`
    /// * `steps_between(&a, &b) == Some(n)` if and only if `b.backward(n) == Some(a)`
    fn steps_between(start: &Self, end: &Self) -> Option<usize>;

    /// Returns the value that would be obtained by taking the *successor* of `self`,
    /// `step_count` times.
    ///
    /// Returns `None` if this would overflow the range of values supported by the type `Self`.
    ///
    /// Note: `step_count == 1` is a common case,
    /// used for example in `Iterator::next` for ranges.
    ///
    /// This must hold for any `a`, `n`, and `m` where `n + m` doesn’t overflow:
    ///
    /// * `a.forward(n).and_then(|x| x.forward(m)) == a.forward(n + m)`
    fn forward(&self, step_count: usize) -> Option<Self>;

    /// Returns the value that would be obtained by taking the *predecessor* of `self`,
    /// `step_count` times.
    ///
    /// Returns `None` if this would overflow the range of values supported by the type `Self`.
    ///
    /// Note: `step_count == 1` is a common case,
    /// used for example in `Iterator::next_back` for ranges.
    ///
    /// This must hold for any `a`, `n`, and `m` where `n + m` doesn’t overflow:
    ///
    /// * `a.backward(n).and_then(|x| x.backward(m)) == a.backward(n + m)`
    fn backward(&self, step_count: usize) -> Option<Self>;
}

Arithmetic and overflow handling with multiple integer types of different widths and signedness is tricky, careful review would be appreciated.

Other changes:

  • The unsafe trait TrustedLen is now implemented for ranges of all integer types all T: Step types. Step is now an unsafe trait as a consequence.
  • Check for overflow everywhere. If it turns out the optimizer doesn’t manage to elide some of the checks that could be removed I’ll leave it to someone else to carefully optimize this, preferably without changing behavior. CC #43064
  • Remove ExactSizeIterator impls for RangeInclusive<u16> and RangeInclusive<i16> as they are incorrect on 16-bit platforms. CC #43086 (comment)
  • Don’t reset inclusive ranges to 1...0 after the last iteration. Instead increment or decrement similarly to other iterations. If that would cause overflow, decrement/increment the other bound instead.
  • Return (usize::MAX, None) instead of (0, None) in size_hint when the exact result would overflow usize.
@rust-highfive

This comment has been minimized.

Copy link
Collaborator

rust-highfive commented Jul 8, 2017

Thanks for the pull request, and welcome! The Rust team is excited to review your changes, and you should hear from @aturon (or someone else) soon.

If any changes to this PR are deemed necessary, please add them as extra commits. This ensures that the reviewer can see what has changed since they last reviewed the code. Due to the way GitHub handles out-of-date commits, this should also make it reasonably obvious what issues have or haven't been addressed. Large or tricky changes may require several passes of review and changes.

Please see the contribution instructions for more information.

@frewsxcv frewsxcv added the T-libs label Jul 9, 2017

@SimonSapin SimonSapin force-pushed the SimonSapin:ranges branch 2 times, most recently from d6e1fd7 to 24cd69a Jul 9, 2017

@SimonSapin

This comment has been minimized.

Copy link
Contributor Author

SimonSapin commented Jul 9, 2017

https://travis-ci.org/rust-lang/rust/jobs/251678510 "env": "IMAGE=x86_64-gnu-llvm-3.7 ALLOW_PR=1 RUST_BACKTRACE=1"

error: linking with `cc` failed: exit code: 1
[…]
undefined reference to `core::option::expect_failed::h6f9984c44d33d89c

🤔

@kennytm

This comment has been minimized.

Copy link
Member

kennytm commented Jul 9, 2017

Merging this as-is would make the situation in #43124 even worse — the speed of (0 .. u16::MAX).collect::<Vec<_>>() is reduced from ~110µs/iter to ~170µs/iter.


Edit: However, changing the implementation of forward() from:

                #[inline]
                fn forward(&self, n: usize) -> Option<Self> {
                    match Self::try_from(n) {
                        Ok(n_converted) => self.checked_add(n_converted),
                        Err(_) => None,  // if n is out of range, `something_unsigned + n` is too
                    }
                }

to:

    #[inline]
    fn forward(&self, n: usize) -> Option<Self> {
        if n <= u16::MAX as usize {
            self.checked_add(n as u16)
        } else {
            None
        }
    }

would immediately fix #43124 (speed becomes ~4µs/iter). I don't understand what LLVM is thinking 🤔


Edit²: Putting the TryFrom definition directly inside the user crate also fixes the issue. Some inlining magic?

@oyvindln

This comment has been minimized.

Copy link
Contributor

oyvindln commented Jul 9, 2017

I don't understand what LLVM is thinking

The impl for TryFrom casts the value to i/u128 to check if it's in range, maybe that is something that could cause issues as the 128-bit types are somewhat special.

@SimonSapin

This comment has been minimized.

Copy link
Contributor Author

SimonSapin commented Jul 9, 2017

I think the current state of this PR is about the amount of effort I’m willing to put into this at the moment. If anyone would like to pick it up and push on the optimization front, feel free.

(I think going through 128-bit ints in TryFrom is unnecessary if we use separate code for size_of::<$Source>() < size_of::<$Destination>() vs size_of::<$Source>() > size_of::<$Destination>().)

@carols10cents

This comment has been minimized.

Copy link
Member

carols10cents commented Jul 10, 2017

o.O I'm not sure why travis is failing like this with linking problems: https://travis-ci.org/rust-lang/rust/jobs/251678510#L1044

I tried restarting that job to make sure it wasn't something intermittent but it just failed in the same way again...

do you have any ideas why this might be happening for this PR @SimonSapin ?

@ExpHP

This comment has been minimized.

Copy link
Contributor

ExpHP commented Jul 10, 2017

/// Returns the number of *successor* steps needed to get from `start` to `end`.
///
/// Returns `None` if that number would overflow `usize`
/// (or is infinite, if `end` would never be reached).
///
/// This must hold for any `a`, `b`, and `n`:
///
/// * `steps_between(&a, &b) == Some(0)` if and only if `a >= b`.
/// * `steps_between(&a, &b) == Some(n)` if and only if `a.forward(n) == Some(b)`
/// * `steps_between(&a, &b) == Some(n)` if and only if `b.backward(n) == Some(a)`

Surely this couId be better worded. I had to read this docstring very carefully to convince myself that it does indeed follow the typical rust semantics of half-open ranges, and that e.g. steps_between(&0, &n) == Some(n)

@SimonSapin

This comment has been minimized.

Copy link
Contributor Author

SimonSapin commented Jul 10, 2017

@carols10cents On IRC @eddyb said it’s probably because intrinsics for u128 divison use a range iterator but are not allowed to link to panic code, and this PR adds calls to Option::expect in range iterator implementations. (Even if these "should" never panic, merely linking causes issues if that code is not optimized away.)

So it sounds like this needs more work to have correct overflow semantics without using panicking code.

@ExpHP I don’t mind rewriting this if you have a suggestion how. I avoided talking about addition and subtraction because Step can be implemented for things that are not necessarily numbers.

@ExpHP

This comment has been minimized.

Copy link
Contributor

ExpHP commented Jul 10, 2017

@ExpHP I don’t mind rewriting this if you have a suggestion how. I avoided talking about addition and subtraction because Step can be implemented for things that are not necessarily numbers.

I would suggest making reference to the "length of the range a..b," which should already be readily familiar to almost any reader, and also places the trait in context of where it is used. As far as I can tell, the only difference is that sometimes this function returns None.

// This seems redundant with a similar comparison that `Step::steps_between`
// implementations need to do, but it separates the `start > end` case from `start == end`.
// `steps_between` returns `Some(0)` in both of these cases, but we only want to add 1
// in the latter.

This comment has been minimized.

@scottmcm

scottmcm Jul 15, 2017

Member

Should steps_between perhaps instead return something like Result<Option<usize>, Unreachable>? Might make the documentation for the method easier to write, too.

/// Returns `None` if this would overflow the range of values supported by the type `Self`.
///
/// Note: `step_count == 1` is a common case,
/// used for example in `Iterator::next` for ranges.

This comment has been minimized.

@scottmcm

scottmcm Jul 15, 2017

Member

Would it be worth a fn forward_one(&self) -> Option<Self> { self.forward(1) } (or call it successor, perhaps, like the comment here does) so the common case is overridable if needed? I suppose it's always addable later if needed.

@@ -280,7 +353,8 @@ impl<A: Step> Iterator for ops::RangeFrom<A> {

#[inline]
fn next(&mut self) -> Option<A> {
let mut n = self.start.add_one();
// Overflow can happen here. Panic when it does.
let mut n = self.start.forward(1).expect("overflow in RangeFrom::next");

This comment has been minimized.

@scottmcm

scottmcm Jul 15, 2017

Member

IIRC this used to only panic in debug, right?

I like the change, but it might need to be called out in documentation somewhere.

@scottmcm

This comment has been minimized.

Copy link
Member

scottmcm commented Jul 15, 2017

Thanks for picking this up, @SimonSapin! The trait looks way better now.

Some general musings on the trait:

  • steps_between is very similar to PartialOrd::cmp, and could be a superset. It might be interesting to remove the need for PartialOrd using that fact.
  • Some of the range RFCs talked about ranges of slightly weird things like singly-linked list or DAG nodes which don't have computable or unique predecessors (respectively). Would it be worth splitting the trait in two, like iterator? (Now, I have no idea how to efficiently implement steps_between for those things, so it might be a moot point.)
@SimonSapin

This comment has been minimized.

Copy link
Contributor Author

SimonSapin commented Jul 16, 2017

Some of the range RFCs talked about ranges of slightly weird things like singly-linked list or DAG nodes which don't have computable or unique predecessors (respectively). Would it be worth splitting the trait in two, like iterator?

So things that have a successor but no predecessor? Yes, this would require a trait separate from Step for the DoubleEndedIterator impl. But honestly this sounds so niche that I’m not convinced std should "go out of its way" to support it. Libraries with such types can define their own ranges and iterators.

@carols10cents

This comment has been minimized.

Copy link
Member

carols10cents commented Jul 24, 2017

friendly ping to keep this on your radar @SimonSapin !

SimonSapin added some commits Jul 6, 2017

SimonSapin added some commits Jul 7, 2017

Remove ExactSizeIterator impls for RangeInclusive<{i16,u16}>
They are incorrect on 16-bit platforms since the return value
of `len()` might overflow `usize`.

Impls for `Range<u32>` and `Range<i32>` are similarly incorrect,
but were stabilized in Rust 1.0.0
so removing them would be a breaking change.

`(0..66_000_u32).len()` for example will compile
without error or warnings on 16-bit platforms, but panic at run-time.

@SimonSapin SimonSapin force-pushed the SimonSapin:ranges branch from 24cd69a to 0ac7fca Jul 24, 2017

@SimonSapin

This comment has been minimized.

Copy link
Contributor Author

SimonSapin commented Jul 24, 2017

My understanding is that the main concern at this point is performance, and I wrote:

#43127 (comment)

I think the current state of this PR is about the amount of effort I’m willing to put into this at the moment. If anyone would like to pick it up and push on the optimization front, feel free.

As to the CI failure, it might be fixed by #43258. I’ve pushed a rebase to trigger a new build.

@SimonSapin

This comment has been minimized.

Copy link
Contributor Author

SimonSapin commented Jul 24, 2017

Perf might be worth re-evaluating once #43248 or #43194 lands.

@alexcrichton

This comment has been minimized.

Copy link
Member

alexcrichton commented Jul 27, 2017

ping @aturon for a review!

@SimonSapin

This comment has been minimized.

Copy link
Contributor Author

SimonSapin commented Jul 27, 2017

This needs some more work that I’m not planning to do soon, closing to make that clear. Hopefully someone else can take over.

  • In one Travis-CI configuration, "Building stage0 compiler artifacts" is failing at link-time with errors like undefined reference to `core::option::expect_failed::h6f9984c44d33d89c'. Eddyb said this might be because u128 division intrinsics use range iterators, and intrinsics are not allowed to link to panicking code. This PR introduces usage of Option::expect in range iterators, although for primitive integers types these should never panic (unless libcore is buggy). I think removing usage of Option::expect would require making the Step trait more complex, effectively undoing some of what this PR does. Maybe the intrinsics can be changed to not use range iterators instead?
  • Several people have expressed concerns about performance. This needs benchmarking to see if this PR regresses it, maybe analyzing assembly code to figure out why and what can be done about it. If it turns out that overflow checking is responsible, consider how much overflow checking is desirable.

@SimonSapin SimonSapin closed this Jul 27, 2017

bors added a commit that referenced this pull request Aug 9, 2017

Auto merge of #43595 - oyvindln:master, r=aturon
Add an overflow check in the Iter::next() impl for Range<_> to help with vectorization.

This helps with vectorization in some cases, such as (0..u16::MAX).collect::<Vec<u16>>(),
 as LLVM is able to change the loop condition to use equality instead of less than and should help with #43124. (See also my [last comment](#43124 (comment)) there.) This PR makes collect on ranges of u16, i16, i8, and u8 **significantly** faster (at least on x86-64 and i686), and pretty close, though not quite equivalent to a [manual unsafe implementation](https://is.gd/nkoecB). 32 ( and 64-bit values on x86-64) bit values were already vectorized without this change, and they still are. This PR doesn't seem to help with 64-bit values on i686, as they still don't vectorize well compared to doing a manual loop.

I'm a bit unsure if this was the best way of implementing this, I tried to do it with as little changes as possible and avoided changing the step trait and the behavior in RangeFrom (I'll leave that for others like #43127 to discuss wider changes to the trait). I tried simply changing the comparison to `self.start != self.end` though that made the compiler segfault when compiling stage0, so I went with this method instead for now.

As for `next_back()`, reverse ranges seem to optimise properly already.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.