-
Notifications
You must be signed in to change notification settings - Fork 12.8k
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
Conversation
src/libcore/slice/mod.rs
Outdated
accum = f(accum, $mkref!(self.ptr.post_inc())); | ||
} | ||
} | ||
accum |
There was a problem hiding this comment.
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.
src/libcore/slice/mod.rs
Outdated
if mem::size_of::<T>() != 0 { | ||
assume(!self.ptr.is_null()); | ||
assume(self.ptr <= self.end); | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
src/libcore/iter/iterator.rs
Outdated
/// 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fold → try_fold here.
src/libcore/iter/iterator.rs
Outdated
/// | ||
/// 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
src/libcore/iter/mod.rs
Outdated
@@ -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); |
There was a problem hiding this comment.
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".
src/libcore/iter/iterator.rs
Outdated
self.try_fold((), move |(), x| { | ||
if f(x) { SearchResult::Found(()) } | ||
else { SearchResult::NotFound(()) } | ||
}).is_found() |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
src/libcore/iter/iterator.rs
Outdated
@@ -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)|{ |
There was a problem hiding this comment.
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.
src/libcore/iter/iterator.rs
Outdated
// The addition might panic on overflow | ||
self.try_fold(0, move |i, x| { | ||
if predicate(x) { SearchResult::Found(i) } | ||
else { SearchResult::NotFound(i+1) } |
There was a problem hiding this comment.
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
src/libcore/iter/iterator.rs
Outdated
} | ||
} | ||
|
||
impl<I:Iterator+Sized> SpecIterator for I { |
There was a problem hiding this comment.
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 +
src/libcore/iter/iterator.rs
Outdated
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) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style nit: space around -
.
src/libcore/slice/mod.rs
Outdated
} | ||
} | ||
default | ||
accum |
There was a problem hiding this comment.
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.
src/libcore/iter/iterator.rs
Outdated
/// assert_eq!(it.next(), Some(&40)); | ||
/// ``` | ||
#[inline] | ||
#[unstable(feature = "iterator_try_fold", issue = "88888888")] |
There was a problem hiding this comment.
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 😃.
src/libcore/iter/mod.rs
Outdated
if n == 0 { SearchResult::Found(acc) } | ||
else { SearchResult::NotFound(acc) } | ||
}).into_inner() | ||
} |
There was a problem hiding this comment.
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()
?
There was a problem hiding this comment.
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 :)
5fa3b53
to
04269c9
Compare
src/libcore/iter/mod.rs
Outdated
if *n == 0 { SearchResult::Found(r) } | ||
else { SearchResult::from_try(r) } | ||
}).into_try() | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
)...
There was a problem hiding this comment.
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
src/libcore/iter/mod.rs
Outdated
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
692ecd9
to
4db8332
Compare
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).
4db8332
to
eef4d42
Compare
r? @aturon This is ready, but @rust-highfive didn't give me a reviewer. Please redirect if someone else should review. |
Moving to another libs team member... r? @dtolnay |
@bors r+ |
📌 Commit b5dba91 has been approved by |
⌛ Testing commit b5dba91 with merge 157c9d0fd39ccfc058f7cefc8de7bc98b69741ea... |
💔 Test failed - status-travis |
cargotest of ripgrep failed.
Line 1033 of the test: The two involved methods: |
@scottmcm Hi, it has been a week since last activity. Are you still working on the cause of the ripgrep failure? |
Thanks for the ping, @kennytm; I'd been spending my time on 45676. Any tips for repro'ing the failures? After cloning ripgrep, |
@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. |
It works for me on Fedora. |
@bors retry |
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.
Thanks, @kennytm & @cuviper! Looks like the cargotest job passed this time: https://travis-ci.org/rust-lang/rust/jobs/303410000#L7454 ¯\_(ツ)_/¯ |
☀️ Test successful - status-appveyor, status-travis |
cc @BurntSushi |
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.
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 bothResult
andOption
(:tada: #42526). Thetry_fold
rustdoc examples useOption
and thetry_rfold
ones useResult
.AKA continuing in the vein of PRs #44682 & #44856 for more of
Iterator
.New bench following the pattern from the latter of those:
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 becauseAlwaysOk
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.