-
Notifications
You must be signed in to change notification settings - Fork 506
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
Descendants #836
Descendants #836
Conversation
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 seems definitely useful to me. I feel like an awful lot of parallel patterns wind up descending over trees.
src/iter/descendants.rs
Outdated
/// assert_eq!(v, vec![3, 10, 14, 18]); | ||
/// ``` | ||
/// | ||
pub fn descendants<S, B, I>(root: S, breed: B) -> Descendants<S, B, 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.
I wonder about a different name, like iter::walk_tree
or something?
Do I understand correctly that this makes no guarantees about traversal order? Of course parallel execution is unordered, but it's still visible in fold and reduce. I think it would be a lot stronger if we could specify the traversal and hold to that. It could even be generic pre/in/post-order if the user provided two closures, or one closure returning two things, so they choose what is logically traversed before or after the current item. Then examples like your tree could I'm sure that would make the implementation harder though... |
i'll take a look. note that it's not for binary trees but for arbitrary degrees so the only meaningful orders are pre and post |
the ordering question was interesting. |
hi, so i did two functions with two different orderings. there is an overhead for the prefix order because breed borrows the state so we must consume the whole iterator before consuming s and then loop again on the same data towards the children. i'd like to have some feedback at this point if you can. |
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 looks good to me. I made some documentation suggestions. I think we should introduce a walk_tree
that simply calls walk_tree_postfix
, so that people have something they can use that does not guarantee ordering.
thanks for all your suggestions. it definitely is much cleaner now. |
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.
Looks great! One last nit.
src/iter/walk_tree.rs
Outdated
/// which guarantees a postfix order. | ||
/// If you don't care about ordering, you should use [`walk_tree`], | ||
/// which will use whatever is believed to be fastest. | ||
/// Between siblings, children are reduced in reverse order -- that is, the children that returned last are reduced first. |
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.
The reverse order seems really bizarre -- is that necessary? As a user, I would expect a "normal" pre-order traversal of a binary tree to first visit the node, then the left children, then the right children.
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.
hi, this is the fastest version. since it is a prefix order we need to consume the father before its children. but we need to borrow the father to iterate on children. this means we first need to extend the stack with the children.
because it's a vec the last pushed will be the first poped and this reverses children's order.
other options are:
- use a more complex struct and some unsafe.
- require the iterator to be double ended
- first collect the iterator into a vec and then extend the stack in the right order
to me the only acceptable option is the third one. it comes with a performance hit though.
if you think it would be better to have the standard order then i can re-implement this version.
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.
ok, so i did a quick bench (the one in rayon_demo)
this is the current version:
sum: 382us collect: 683us
this is the version with the "fixed" ordering:
sum: 705us collect: 950us
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.
That speed difference seems pretty significant to me
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.
first collect the iterator into a vec and then extend the stack in the right order
You could do this in-place by noting the previous length, extending, then reversing the new tail slice. That's still some added cost, but at least it doesn't require a new allocation.
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 did it like this :
self.reorder_buffer.extend((self.breed)(&e));
self.to_explore.extend(self.reorder_buffer.drain(..).rev());
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 to be clear, i do understand there is a choice to be made here.
the overhead is between 10 and 15 nanoseconds per element. this is not zero cost but not excessive.
if you ask me i would favor the current (reversed) order because the algorithm is "natural" this way. user can also reverse their iterators to get to the other order. however i do see why the other option also makes sense.
in the end, choose whatever you want and let me know.
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.
Ideally, internal algorithm details should not be so visible in the public API, especially since those details might want to change in the future. I want to aim for what is most "natural" to the user first, without exposing details like "well actually you probably want to reverse this." That's even evident in your tree_prefix_collect
benchmark where you have an explanatory comment, "large indices to the left, small to the right", being different than the others.
To that end, I think DoubleEndedIterator
might be reasonable, although you immediately dismissed that. The user can provide the children in their natural order, matching the visitation order, but we'll have the flexibility to internally iterate in reverse. Are there realistic use-cases that you think could not be double-ended?
Of course there are plenty of non-DE iterators, but I'm asking for a real data structure that could not operate this way, especially since you say "users can also reverse their iterators." If they can so easily reverse, then DE should be no issue; if they can't, then I think we won't be serving them well with an algorithmically-reversed order.
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.
so i did the switch to double ended iterators.
the non double ended iterators i use from time to time are scan and successors but i don't really see how they would be used 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.
I think we now just need to update (or remove) this line about reversal.
i added a bit more tests with higher degrees |
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 is looking pretty good to me.
do i need to do something more ? i'm in no hurry but just tell me if you want something from me. |
I'm satisfied -- @cuviper ? |
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.
There's some minor cleanup, but otherwise I'm satisfied.
Please also take the PR out of draft state if you think it's ready.
src/iter/walk_tree.rs
Outdated
/// which guarantees a postfix order. | ||
/// If you don't care about ordering, you should use [`walk_tree`], | ||
/// which will use whatever is believed to be fastest. | ||
/// Between siblings, children are reduced in reverse order -- that is, the children that returned last are reduced first. |
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 we now just need to update (or remove) this line about reversal.
src/iter/walk_tree.rs
Outdated
S: Send, | ||
B: Fn(&S) -> I + Send + Sync, | ||
IT: DoubleEndedIterator<Item = S>, | ||
I: IntoIterator<Item = S, IntoIter = IT> + Send, |
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 we can remove IT
as a public API parameter by constraining I::IntoIter: Double...
instead.
Should we also require I::IntoIter: Send
? We consume it immediately now, but might there be a future where we'd want to be more lazy about that?
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 not sure i get it. what would send allow us to do ? IT would not be a parallel iterator so it would just mean using another thread. I
is already Send.
so i'm not getting what this would potentially allow us to do and why I
being Send is not enough
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: Send
doesn't actually give us much, only that after getting such a return value of B
, we could hold and send that I
value to other threads before calling into_iter()
. Once that's called, we now have the IntoIter
type, which we might want to lazily pull items out one at a time.
For example, suppose to_explore
were instead a Vec<Either<S, IntoIter>>
, storing either unvisited nodes or their unprocessed (or partially processed) brood. We would need IntoIter: Send
to be able to split this between threads.
Maybe we don't need to do this, especially if we don't expect there to be very many children most of the time.
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.
Actually, I: Send
is only necessary right now because you have that PhantomData<I>
, but I don't think you actually need that. The structs themselves don't need the I
parameter at all, only the impl
constraints.
src/iter/walk_tree.rs
Outdated
where | ||
S: Send, | ||
B: Fn(&S) -> I + Send + Sync, | ||
I: IntoIterator<Item = S> + Send, |
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 question about maybe I::IntoIter: Send
.
src/iter/walk_tree.rs
Outdated
where | ||
S: Send, | ||
B: Fn(&S) -> I + Send + Sync, | ||
I: IntoIterator<Item = S> + Send, |
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 question about maybe I::IntoIter: Send
.
@wagnerf42 should we take the PR out of draft state? |
this commit adds the `descendants` function which is an equivalent of `split` for tree structured data.
sequential folding is recursive i'm not sure how to do it sequentially since this would imply a self borrowing struct (we need to store both the state and the iterator borrowing it)
postfix collect test + bench, removed deque only thing needed now is tests for n-ary trees
removed vectors allocations + no recursion
changed order between siblings for that
Co-authored-by: Niko Matsakis <niko@alum.mit.edu>
Co-authored-by: Niko Matsakis <niko@alum.mit.edu>
Co-authored-by: Niko Matsakis <niko@alum.mit.edu>
Co-authored-by: Niko Matsakis <niko@alum.mit.edu>
- prefix order is reversed, we now required double ended iterators - tests and benches updated accordingly - two more tests for flat trees - removed unneeded malloc in task splitting
tests for graphs with higher degrees
- renamed breed to children_of - doc cleanup - using consume_iter
Otherwise we'll be under-constrained, unable to actually make a choice between the implementations. (Even though we think the faster one doesn't need that right now.)
Hi @wagnerf42! Sorry that it's been a while. I took the liberty of rebasing your PR and making a few of those tweaks I had suggested way back in my last review. Please take a look, and if you're happy with this then I think we should merge it! |
hi, well it seems fine for me. |
Thanks! |
hi,
this is an equivalent of the
split
function but for tree like patterns.it solves an issue that was posted recently and i could also use it with some of my code.
tell me if you think it is worth inclusion for you. if so i can add some more tests and benches.