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

Short-circuiting internal iteration with Iterator::try_fold & try_rfold #45595

Merged
merged 2 commits into from
Nov 17, 2017

Conversation

scottmcm
Copy link
Member

@scottmcm scottmcm commented Oct 28, 2017

These are the core methods in terms of which the other methods (fold, all, any, find, position, nth, ...) can be implemented, allowing Iterator implementors to get the full goodness of internal iteration by only overriding one method (per direction).

Based off the Try trait, so works with both Result and Option (:tada: #42526). The try_fold rustdoc examples use Option and the try_rfold ones use Result.

AKA continuing in the vein of PRs #44682 & #44856 for more of Iterator.

New bench following the pattern from the latter of those:

test iter::bench_take_while_chain_ref_sum          ... bench:   1,130,843 ns/iter (+/- 25,110)
test iter::bench_take_while_chain_sum              ... bench:     362,530 ns/iter (+/- 391)

I also ran the benches without the fold & rfold overrides to test their new default impls, with basically no change. I left them there, though, to take advantage of existing overrides and because AlwaysOk has some sub-optimality due to #43278 (which 45225 should fix).

If you're wondering why there are three type parameters, see issue #45462

Thanks for @bluss for the original IRLO thread and the rfold PR and to @cuviper for adding so many folds, encouraging me to make this PR, and finding a catastrophic bug in a pre-review.

accum = f(accum, $mkref!(self.ptr.post_inc()));
}
}
accum
Copy link
Member

Choose a reason for hiding this comment

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

This fold impl can just be a for loop instead I think.

if mem::size_of::<T>() != 0 {
assume(!self.ptr.is_null());
assume(self.ptr <= self.end);
}
Copy link
Member

Choose a reason for hiding this comment

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

What's the reason these assumes were added here?

Copy link
Member Author

Choose a reason for hiding this comment

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

next and next_back have assumptions, so I put them here too. Given their status as arcana (from the rposition PR), I could certainly remove them...

Copy link
Member

Choose a reason for hiding this comment

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

It's a shame if they should be needed, but I know they are needed for some optimizations in next. We should probably have some evidence for each of them being added. The non-null one is at least used when we create an Option<&T>, and that doesn't happen in this method, so it doesn't seem necessary?

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm, search_while didn't have them, so hopefully they're not needed. I'll remove.

/// An iterator method that applies a function as long as it returns
/// successfully, producing a single, final value.
///
/// `fold()` takes two arguments: an initial value, and a closure with two
Copy link
Member

Choose a reason for hiding this comment

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

fold → try_fold here.

///
/// If possible, override this method with an implementation using
/// internal iteration. Most of the other methods have their default
/// implementation in terms of this one.
Copy link
Member

Choose a reason for hiding this comment

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

Hm, any implementation is by definition using internal iteration? Maybe we can say they should in turn use try_fold on the parts the iterator are composed of, if possible. And some kind of smooth reminder that the implementation must keep all internal state up to date, no matter if it finishes with Ok or Err.

Copy link
Member Author

Choose a reason for hiding this comment

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

I took another stab at this section; hopefully it's on the right track.

@@ -336,6 +337,84 @@ mod range;
mod sources;
mod traits;

/// ZST used to implement foo methods in terms of try_foo
struct AlwaysOk<T>(pub T);
Copy link
Member

@bluss bluss Oct 28, 2017

Choose a reason for hiding this comment

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

ZST is a misnomer (it's just "zero additional size" here), maybe just say "newtype".

self.try_fold((), move |(), x| {
if f(x) { SearchResult::Found(()) }
else { SearchResult::NotFound(()) }
}).is_found()
Copy link
Member

@bluss bluss Oct 28, 2017

Choose a reason for hiding this comment

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

Just curious why all and any are different (one uses SearchResult and the other not)

Copy link
Member Author

Choose a reason for hiding this comment

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

I think it's because my brain thinks in the true case, so all is continue on "success" (Result) but any is break on "success" (SearchResult). That is, of course, a super-fuzzy statement, since the opposites also work just as well.

@@ -1922,10 +1973,10 @@ pub trait Iterator {
let mut ts: FromA = Default::default();
let mut us: FromB = Default::default();

for (t, u) in self {
self.for_each(|(t, u)|{
Copy link
Member

Choose a reason for hiding this comment

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

style nit: space before { please.

// The addition might panic on overflow
self.try_fold(0, move |i, x| {
if predicate(x) { SearchResult::Found(i) }
else { SearchResult::NotFound(i+1) }
Copy link
Member

Choose a reason for hiding this comment

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

style nit: space around + please

}
}

impl<I:Iterator+Sized> SpecIterator for I {
Copy link
Member

Choose a reason for hiding this comment

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

style nit: space after : space around +

fn spec_nth(&mut self, n: usize) -> Option<Self::Item> {
self.try_fold(n, move |i, x| {
if i == 0 { SearchResult::Found(x) }
else { SearchResult::NotFound(i-1) }
Copy link
Member

Choose a reason for hiding this comment

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

style nit: space around -.

}
}
default
accum
Copy link
Member

Choose a reason for hiding this comment

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

same as fold, why not just a for or while let loop here.

@kennytm kennytm added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue. labels Oct 28, 2017
/// assert_eq!(it.next(), Some(&40));
/// ```
#[inline]
#[unstable(feature = "iterator_try_fold", issue = "88888888")]
Copy link
Member

Choose a reason for hiding this comment

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

Now that the tracking issue is filed, please change these to 45594 before we forget 😃.

if n == 0 { SearchResult::Found(acc) }
else { SearchResult::NotFound(acc) }
}).into_inner()
}
Copy link
Member

Choose a reason for hiding this comment

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

can't this fold just use .try_fold()?

Copy link
Member Author

Choose a reason for hiding this comment

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

I was worried about SearchResult::from_try(r) potentially pessimizing things, since it does a bunch of destruct-rebuild.
There may be no basis behind that, though, since it's not the more important case of "fold should use fold to pick up any existing overrides". I'll just delete it, since less code is better :)

@kennytm kennytm added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Oct 28, 2017
if *n == 0 { SearchResult::Found(r) }
else { SearchResult::from_try(r) }
}).into_try()
}
Copy link
Member

Choose a reason for hiding this comment

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

It wasn't obvious to me if this was correct, but I think it is? The inversion of using SearchResult is a bit confusing even if the intention is probably the opposite.

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm, it got named that way because it was first written for find, but then I ended up using it in far more places than just that. I wonder if LoopResult with Break and Continue would be better -- would turn SearchResult::from_try into LoopResult::break_if_error (or continue_if_ok)...

Copy link
Member Author

@scottmcm scottmcm Oct 29, 2017

Choose a reason for hiding this comment

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

I renamed SearchResult, and I think I like the new way better. Agree? Other suggestions?
scottmcm/rust@iter-try-fold...scottmcm:iter-try-fold-experiment

fn try_fold<Acc, F, R>(&mut self, init: Acc, mut f: F) -> R where
Self: Sized, F: FnMut(Acc, Self::Item) -> R, R: Try<Ok=Acc>
{
match self.state {
Copy link
Member

@bluss bluss Oct 28, 2017

Choose a reason for hiding this comment

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

In this method I'm wondering about the benefits of avoiding duplicating expressions that are big loops (self.a.try_fold(init, f) and similar for b). I imagine if these are small enough to inline, they are all inlined and duplicated.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh, binary size. Makes sense; I can share the calls for the states.

Copy link
Member

@bluss bluss Nov 3, 2017

Choose a reason for hiding this comment

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

code size is relevant for the package of cpu-cache-memory's performance too, just to give a broader sense for what binary size means. Thanks for the fix.

accum = f(accum, x);
}
accum
self.try_fold(init, move |acc, x| AlwaysOk(f(acc, x))).0
Copy link
Contributor

@JordiPolo JordiPolo Oct 29, 2017

Choose a reason for hiding this comment

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

I'm fairly new to rust and I know other languages implements their iterator methods based on fold. I imagine the original implementors of these methods know that also and if they use simple for loops is because they are compiler friendly.
Creating the closure here + if let, etc. In try_fold. Unless the compiler is really really good it will create slower code, no?
Have you tried iterating on more complex data than just integers?

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 it's more that a for loop was the obvious way to do it, and the emphasis on internal iteration is a more recent idea. For simple iterators, it should be a wash, but iterators like Chain can lift their conditionals out in a fold or try_fold, better than repeated next calls.

Copy link
Member Author

Choose a reason for hiding this comment

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

@JordiPolo Here's a link to explore how this gets translated: https://godbolt.org/g/3ehFBV

There are a few things interacting here to make it relatively straight-forward for the compiler to turn this into good code. Note the definition of the AlwaysOk type:

struct AlwaysOk<T>(pub T);

That means that wrapping something in AlwaysOk is actually not doing anything -- the memory layout doesn't change at all. (Asterisk for potential ABI implications and that repr(rust) layout is subject to change, but that shouldn't be relevant in this case.) Similarly, the .0 at the end is also a type-level-only thing, as it doesn't need to change the representation at all.

The other thing that the compiler needs to be able to do is to know that the ? operators in the try_fold materialization will never return early. But see the Try impl:

impl<T> Try for AlwaysOk<T> {
    type Ok = T;
    type Error = !;
    ...
}

The "never type" ! there is the canonical uninhabited type. (There are others, like if you define enum NoVariants {}.) Because uninhabited types have no valid values, it knows that an error can never happen, so it can completely remove the early-return paths, making it equivalent to normal fold.

Copy link
Member

Choose a reason for hiding this comment

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

@JordiPolo You're right, it's asking the compiler to inline a lot, but it's not a miraculous effort, since closures like other things in Rust default to being unboxed.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks so much for the details, I think ! is the magic I needed to understand, now I see the logic

@kennytm kennytm added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Oct 29, 2017
This is the core method in terms of which the other methods (fold, all, any, find, position, nth, ...) can be implemented, allowing Iterator implementors to get the full goodness of internal iteration by only overriding one method (per direction).
@scottmcm
Copy link
Member Author

r? @aturon

This is ready, but @rust-highfive didn't give me a reviewer. Please redirect if someone else should review.

@shepmaster
Copy link
Member

Moving to another libs team member...

r? @dtolnay

@rust-highfive rust-highfive assigned dtolnay and unassigned aturon Nov 3, 2017
@dtolnay
Copy link
Member

dtolnay commented Nov 7, 2017

@bors r+

@bors
Copy link
Contributor

bors commented Nov 7, 2017

📌 Commit b5dba91 has been approved by dtolnay

@bors
Copy link
Contributor

bors commented Nov 8, 2017

⌛ Testing commit b5dba91 with merge 157c9d0fd39ccfc058f7cefc8de7bc98b69741ea...

@bors
Copy link
Contributor

bors commented Nov 8, 2017

💔 Test failed - status-travis

@kennytm
Copy link
Member

kennytm commented Nov 8, 2017

cargotest of ripgrep failed.

[00:56:32] failures:
[00:56:32] 
[00:56:32] ---- feature_45_relative_cwd stdout ----
[00:56:32] 	thread 'feature_45_relative_cwd' panicked at 'assertion failed: `(left == right)`
[00:56:32]   left: `["bar/test", "baz/bar/test", "baz/foo", "baz/test", "foo", "test"]`,
[00:56:32]  right: `["bar/test", "baz/bar/test", "baz/baz/bar/test", "baz/foo", "baz/test", "foo", "test"]`', tests/tests.rs:1033:4
[00:56:32] note: Run with `RUST_BACKTRACE=1` for a backtrace.
[00:56:32] 
[00:56:32] 
[00:56:32] failures:
[00:56:32]     feature_45_relative_cwd
[00:56:32] 
[00:56:32] test result: FAILED. 101 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
[00:56:32] 
[00:56:32] error: test failed, to rerun pass '--test integration'
[00:56:32] thread 'main' panicked at 'tests failed for https://github.com/BurntSushi/ripgrep', /checkout/src/tools/cargotest/main.rs:100:8
[00:56:32] note: Run with `RUST_BACKTRACE=1` for a backtrace.

Line 1033 of the test:

https://github.com/BurntSushi/ripgrep/blob/b65bb37b14655e1a89c7cd19c8b011ef3e312791/tests/tests.rs#L1033

The two involved methods:

https://github.com/BurntSushi/ripgrep/blob/b65bb37b14655e1a89c7cd19c8b011ef3e312791/tests/tests.rs#L57-L69

@kennytm kennytm added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Nov 8, 2017
@kennytm
Copy link
Member

kennytm commented Nov 15, 2017

@scottmcm Hi, it has been a week since last activity. Are you still working on the cause of the ripgrep failure?

@scottmcm
Copy link
Member Author

Thanks for the ping, @kennytm; I'd been spending my time on 45676.

Any tips for repro'ing the failures? After cloning ripgrep, git checkout b65bb37b14655e1a8 && cargo +stage2 test is completing successfully for me. (I'm on windows; could that matter?)

@kennytm
Copy link
Member

kennytm commented Nov 16, 2017

@scottmcm It could matter since the failure happens on Linux. If it is still not reproducible on Linux, we could ask bors to retry assuming ripgrep's test is flaky.

@cuviper
Copy link
Member

cuviper commented Nov 16, 2017

It works for me on Fedora.

@kennytm
Copy link
Member

kennytm commented Nov 17, 2017

@bors retry

@bors
Copy link
Contributor

bors commented Nov 17, 2017

⌛ Testing commit b5dba91 with merge b32267f...

bors added a commit that referenced this pull request Nov 17, 2017
Short-circuiting internal iteration with Iterator::try_fold & try_rfold

These are the core methods in terms of which the other methods (`fold`, `all`, `any`, `find`, `position`, `nth`, ...) can be implemented, allowing Iterator implementors to get the full goodness of internal iteration by only overriding one method (per direction).

Based off the `Try` trait, so works with both `Result` and `Option` (:tada: #42526).  The `try_fold` rustdoc examples use `Option` and the `try_rfold` ones use `Result`.

AKA continuing in the vein of PRs #44682 & #44856 for more of `Iterator`.

New bench following the pattern from the latter of those:
```
test iter::bench_take_while_chain_ref_sum          ... bench:   1,130,843 ns/iter (+/- 25,110)
test iter::bench_take_while_chain_sum              ... bench:     362,530 ns/iter (+/- 391)
```

I also ran the benches without the `fold` & `rfold` overrides to test their new default impls, with basically no change.  I left them there, though, to take advantage of existing overrides and because `AlwaysOk` has some sub-optimality due to #43278 (which 45225 should fix).

If you're wondering why there are three type parameters, see issue #45462

Thanks for @bluss for the [original IRLO thread](https://internals.rust-lang.org/t/pre-rfc-fold-ok-is-composable-internal-iteration/4434) and the rfold PR and to @cuviper for adding so many folds, [encouraging me](#45379 (comment)) to make this PR, and finding a catastrophic bug in a pre-review.
@scottmcm
Copy link
Member Author

scottmcm commented Nov 17, 2017

Thanks, @kennytm & @cuviper! Looks like the cargotest job passed this time: https://travis-ci.org/rust-lang/rust/jobs/303410000#L7454 ¯\_(ツ)_/¯

@bors
Copy link
Contributor

bors commented Nov 17, 2017

☀️ Test successful - status-appveyor, status-travis
Approved by: dtolnay
Pushing b32267f to master...

@bors bors merged commit b5dba91 into rust-lang:master Nov 17, 2017
@scottmcm scottmcm deleted the iter-try-fold branch November 17, 2017 10:12
@kennytm
Copy link
Member

kennytm commented Nov 17, 2017

cc @BurntSushi

bors added a commit that referenced this pull request Nov 19, 2017
Undo the Sized specialization from Iterator::nth

I just added this as part of #45595, but I'm now afraid there's a specialization issue with it, since I tried to add [another similar specialization](https://github.com/rust-lang/rust/compare/master...scottmcm:faster-iter-by-ref?expand=1#diff-1398f322bc563592215b583e9b0ba936R2390), and ended up getting really disturbing test failures like
```
thread 'iter::test_by_ref_folds' panicked at 'assertion failed: `(left == right)`
  left: `15`,
 right: `15`', src\libcore\../libcore/tests\iter.rs:1720:4
```

So since this wasn't the most critical part of the change and a new beta is branching within a week, I think putting this part back to what it was before is the best option.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants