Skip to content

make all_equal() faster #342

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

Merged
merged 2 commits into from
Aug 20, 2019
Merged

make all_equal() faster #342

merged 2 commits into from
Aug 20, 2019

Conversation

fyrchik
Copy link
Contributor

@fyrchik fyrchik commented Apr 26, 2019

Hello!
This PR adresses #282 issue. Variant with dedup does not short circuit, but one with all does.
I have also added some benchmarks and test for an empty iterator.

test all_equal                                ... bench:     999,832 ns/iter (+/- 217,245)
test all_equal_default                        ... bench:   4,814,277 ns/iter (+/- 315,335)
test all_equal_for                            ... bench:   2,096,174 ns/iter (+/- 165,596)

Let me know, what do you think.

@timvermeulen
Copy link

Looks good!

@axelf4
Copy link

axelf4 commented Jun 23, 2019

An alternative implementation could be:

self.try_fold(None, |acc, x| acc.filter(|&prev| x != prev).xor(Some(Some(x))))

@timvermeulen
Copy link

@axelf4 I haven't done any benchmarks, have you by any chance? I'd be worried that that would perform worse than this PR's implementation because of the extra branching in the closure body. But it's very hard to tell without benchmarks.

@jswrenn jswrenn self-assigned this Jul 18, 2019
Copy link
Member

@phimuemue phimuemue left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me.

@@ -1310,9 +1310,13 @@ pub trait Itertools : Iterator {
/// assert!(data.into_iter().all_equal());
/// ```
fn all_equal(&mut self) -> bool
where Self::Item: PartialEq,
where Self: Sized,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think requiring Sized is ok. Can anyone with more expertise confirm this is unproblematic?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are already lots of methods you can't call on an unsized iterator, so I don't think this is problematic.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little mystified. The documentation for Iterator::all indicates that the only bound is F: FnMut(Self::Item) -> bool, but the implementation of Iterator::all also requires Self: Sized. Why is this bound elided from the documentation?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the documentation (sometimes?) leaves out Self: Sized bounds because they’re so common 😕

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per Reddit, I think this is a bug. I've filed an issue: rust-lang/rust#62899.

@@ -100,6 +100,7 @@ fn dedup() {

#[test]
fn all_equal() {
assert!("".chars().all_equal());
Copy link
Member

@phimuemue phimuemue Jul 20, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea to include this corner case. Should we add the "one element" corner case, too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it will be a nice addtition. I have added it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a quickcheck test would be a great alternative to either of those (something for another PR)

@bluss
Copy link
Member

bluss commented Aug 20, 2019

This should not need to require Self: Sized, and I'd prefer to rewrite it so that it is not a breaking change (in my opinion).

I'm not sure I understand the short circuit argument - in what sense does this short circuit more than dedup does? They should both visit the same number of iterator elements.

@timvermeulen
Copy link

@bluss The dedup version does also short-circuit, but only after the second "group" has been iterated entirely, rather than right after the first element of the second group.

@bluss
Copy link
Member

bluss commented Aug 20, 2019

Oh. We are long overdue for this improvement then.

One major part of benchmark speed up could be that all is explicitly unrolled in the slice iterator (slice iterator specific, in its try_fold). That's nice to use, but we can note that it is not a general improvement. In addition to that we have the improvement of using internal iteration (all/try_fold etc) which is a bit more general.

Looking at those upsides it seems ok to have a breaking change. I'm not sure in what context it would be a detectable breaking change, it should only change which implementation is picked when we call this method on a trait object.

... and it turns out the trait object question is moot, because trait object support has been broken in Itertools 0.8.0, dyn Itertools is not possible at the moment precisely because of some missing Self: Sized bounds.

That's something we could revisit, but let's only add back trait object support if we can find a good reason. Clearly there was no test for it.

With that, I don't think adding Self: Sized is a breaking change.

@jswrenn
Copy link
Member

jswrenn commented Aug 20, 2019

If the trait object question is moot, I'm happy to merge this!

bors r+

@jswrenn
Copy link
Member

jswrenn commented Aug 20, 2019

bors r+

bors bot added a commit that referenced this pull request Aug 20, 2019
342: make all_equal() faster r=jswrenn a=fyrchik

Hello!
This PR adresses #282 issue. Variant with `dedup` does not short circuit, but one with `all` does.
I have also added some benchmarks and test for an empty iterator.
```
test all_equal                                ... bench:     999,832 ns/iter (+/- 217,245)
test all_equal_default                        ... bench:   4,814,277 ns/iter (+/- 315,335)
test all_equal_for                            ... bench:   2,096,174 ns/iter (+/- 165,596)
```

Let me know, what do you think.

Co-authored-by: Evgenii <[email protected]>
@bors
Copy link
Contributor

bors bot commented Aug 20, 2019

Build succeeded

@bors bors bot merged commit 99002dd into rust-itertools:master Aug 20, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants