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

Allow Pre-Stepping in Step-By? #52065

Closed
Emerentius opened this issue Jul 5, 2018 · 9 comments
Closed

Allow Pre-Stepping in Step-By? #52065

Emerentius opened this issue Jul 5, 2018 · 9 comments
Labels
I-slow Issue: Problems and improvements with respect to performance of generated code. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.

Comments

@Emerentius
Copy link
Contributor

step_by has to special case the first element and this behaviour can act as a performance block because of the necessary branch in next as seen with integer ranges. This branch can be removed, by always taking the next element and also stepping ahead in one go but that changes the semantics when the iterator has side-effects.
step_by will be stabilized with the next release and I fear that the need to preserve semantics may lock us into a sub-optimally performing step_by or some other lacking solution. I propose that we amend a note to step_by, allowing us to use the "always take and step ahead" semantics for optimization for now. In the future, when we have this issue figured out, we can then limit the cases where pre-stepping is allowed. The general case should not use it.

One example where preserving semantics causes inefficiency is RangeFrom iterators. When pre-stepping, a RangeFrom will overflow its start member one iteration earlier and will therefore, in debug mode, panic one iteration earlier. That may cause a panic in a program that doesn't actually use any elements after the overflow, i.e. a program without any user error. If we have to preserve these semantics, RangeFrom can not be optimized for step_by.
(RangeFrom reserves the right to change overflow behaviour so we could "fix" this particular one by allowing overflow in debug mode)

Furthermore, if range iterators need to pre-step for performance reasons, then the currently still unstable Step trait's step* methods must be limited to be side-effect free or the pre-stepping optimization would be invalid.

@oli-obk oli-obk added I-slow Issue: Problems and improvements with respect to performance of generated code. I-nominated T-libs-api Relevant to the library API team, which will review and decide on the PR/issue. labels Jul 5, 2018
@oli-obk
Copy link
Contributor

oli-obk commented Jul 5, 2018

Nominated for discussion by @rust-lang/libs because of upcoming stabilization being a blocker for perf improvements due to semantic changes being required

@Emerentius I think discussion would be aided by a PR that makes this change and shows some performance numbers

@SimonSapin SimonSapin added the beta-nominated Nominated for backporting to the compiler in the beta channel. label Jul 5, 2018
@SimonSapin
Copy link
Contributor

1.28 is in beta, so if we make a change it may need to be backported.

I’m having a little hard time following what is being proposed exactly. Could you give some code examples and the behavior before/after the change?

Is the change "only" in documentation of semantics, in terms of what implementations of a trait are allowed or not allowed to do? Is that the Iterator trait, the Step trait, both? Could you provide wording that would go into those docs?

@Emerentius
Copy link
Contributor Author

I'm not proposing any code changes right now, only a note in the docs giving us some freedom for future changes.

The semantics of step_by are (next), (skip, next), (skip, next), …. Parenthesised actions are grouped, i.e. done together.
In Rust, that is next(), nth(), nth()…. That necessitates a branch because of the first element. With side-effect free iterators you can switch this to a branchless (next, skip), (next, skip), …. That's what I've been calling the pre-stepping semantics.
We don't (yet?) have a function for (next, skip) except for a combination of next and nth.

The concrete performance problem I have in mind are still ranges (#51557, #43064, #31155). Their problems are not entirely due to StepBy but its branching is part of the issue. For now, StepBy<Range> and StepBy<RangeInclusive> are specialized to use the branchless pre-stepping. It's hacky, but it covers the most common cases. RangeFrom can't be specialized because it would make a correct program crash in an edge case if the current semantics have to be conserved. I'd rather not have to tell people to never use (start..).step_by() without for_each or another iter consumer.

The problem therefore is, what if we need to be able to pre-step for performance reasons in the presence of side-effects? With RangeFrom, that means an integer overflow, but in the future the question extends to any implementor of the Step trait. User iterators could also gain from this, but I have no examples currently.
We shouldn't pre-step in general precisely because of an iterator's possible side-effects, but we may need some way for iterators to opt into it. Special casing only ranges seems questionable.

I just want to keep our options open for now but change no actual code. We can sort this out without a stabilization countdown over our heads. Also, I don't have much time right now.

@alexcrichton
Copy link
Member

@Emerentius could you expand on the proposed documentation change? What do you think should be added to the documentation to future-proof ourselves?

@scottmcm
Copy link
Member

scottmcm commented Jul 7, 2018

Right now, the following code

    (0..10)
        .inspect(|x| print!("({}) ", x))
        .step_by(2)
        .take(3)
        .for_each(|x| println!("{}", x));

prints

(0) 0
(1) (2) 2
(3) (4) 4

I believe the request is to allow it the freedom to instead print

(0) (1) 0
(2) (3) 2
(4) (5) 4

Which I think can be optimized better for ranges (one addition instead of two) and could be argued is a more pure translation of "step by two", but has the obvious downside of potentially reading more things from the source iterator (if it doesn't get exhausted).

@alexcrichton
Copy link
Member

Ah ok, thanks @scottmcm! That makes total sense to me and I'd personally be down for such an implementation

@Emerentius
Copy link
Contributor Author

Emerentius commented Jul 8, 2018

Yes, that's what I was trying to say. It's a bit difficult to express concisely in terms of existing iterator methods. Here's my attempt at the note to be added:


Note: StepBy behaves like the sequence next(), nth(step-1), nth(step-1), …, but is also free to behave like the sequence advance_n_and_return_first(step), advance_n_and_return_first(step), …
Which way is used may change for some iterators for performance reasons. The second way will advance the iterator earlier and may consume more items.

advance_n_and_return_first is the equivalent of:

fn advance_n_and_return_first<I>(iter: &mut I, total_step: usize) -> Option<I::Item>
where
    I: Iterator,
{
    let next = iter.next();
    if total_step > 1 {
        iter.nth(step-2);
    }
    next
}

@alexcrichton
Copy link
Member

Sounds like a great clause to me to add!

(I'd personally be down for a PR to change the behavior as well)

@Emerentius
Copy link
Contributor Author

PR is merged and backported

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
I-slow Issue: Problems and improvements with respect to performance of generated code. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.
Projects
None yet
Development

No branches or pull requests

5 participants