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

Implement find_first/last, position_first/last #189

Merged
merged 12 commits into from
Dec 31, 2016

Conversation

schuster
Copy link
Contributor

Fixes #151

This is my first PR to Rayon, so any and all feedback is welcome.

@cuviper
Copy link
Member

cuviper commented Dec 20, 2016

It looks like you're missing some imports for the stable compilers, possibly related to ordering like #173.
(IOW, try adding use super::internal::*; before use super::*;)

Copy link
Member

@cuviper cuviper left a comment

Choose a reason for hiding this comment

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

This looks really nice! I have only a few comments...

// divide the range in half
let old_lower_bound = self.lower_bound.get();
let median = old_lower_bound + ((self.upper_bound - old_lower_bound) / 2);
self.lower_bound.set(median);
Copy link
Member

Choose a reason for hiding this comment

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

So, it's important to your algorithm that the result from split_off is always used for the left/lower side by whatever is calling it. Near as I can tell, that's always true in practice, but I don't think we've ever specified it must be. It's just generally convenient to call in order (consumer.split_off(), consumer) when splitting in two.

I don't think there's a problem here, but we need to be careful, and this deserves a comment at least.

Copy link
Member

Choose a reason for hiding this comment

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

Perhaps we can rename split_off() to split_left() or split_off_left() or split_left_off(), something like that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point; I'll add a comment. +1 to renaming split_off().

// before, depending on timing. This means more consumers will
// continue to run than necessary, but the reducer will still ensure
// the correct value is returned.
self.best_found.swap(self.boundary, Ordering::Relaxed);
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 we can do better than that with a compare_and_swap loop, repeating until you either get a successful swap or find that someone else swapped something "better" than you anyway.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, seems like it'd be worth using a compare-exchange here. I'd probably use compare_exchange_weak, but that's a minor thing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was indeed wondering whether a loop like you describe would be better. I'll write that up and push it.

fn full(&self) -> bool {
let best_found = self.best_found.load(Ordering::Relaxed);
match self.match_position {
MatchPosition::Leftmost => best_found < self.boundary,
Copy link
Member

Choose a reason for hiding this comment

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

For Leftmost only, isn't self.item.is_some() also sufficient?

Copy link
Member

Choose a reason for hiding this comment

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

For that matter, consume probably needs to make sure that Leftmost doesn't clobber any existing self.item. We treat full as a hint, but consume should ensure its own correctness when that's important. Maybe try some tests with many successive candidates that will hit the same folder, perhaps even find_first(|| true).

Copy link
Member

Choose a reason for hiding this comment

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

Wait, am I confused? Why is self.item() sufficient? Don't we want to also stop if some other folder (to our left) has found an item?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@nikomatsakis I think he's saying it's also sufficient, which is correct (and a case I didn't think about). If we've already found the item, we need to make sure we stop before we find some other item slightly to the right.

So @cuviper, I think you're right that that logic should be added to both full and consume.

Copy link
Member

Choose a reason for hiding this comment

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

Right, it's an additional case for stopping.

Copy link
Member

Choose a reason for hiding this comment

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

yes, ok. I agree (this seems to relate to having some tests that cover this case)

use super::*;
use super::len::*;

// The consumer for find_first/find_last has fake indexes representing the lower
Copy link
Member

Choose a reason for hiding this comment

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

👍 to leaving some comments on how things work =)

// consumer's position relative to other consumers. The purpose is to allow a
// consumer to know it should stop consuming items when another consumer finds a
// better match.

Copy link
Member

Choose a reason for hiding this comment

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

Total nit: I'd like a // here to indicate that these are two paragraphs in the same comment.

// don't implement that for now. The only downside of the current approach is
// that in some cases, iterators very close to each other will have the same
// range and therefore not be able to stop processing if one of them finds a
// better match than the others.
Copy link
Member

Choose a reason for hiding this comment

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

Hmm, can you elaborate here?

Copy link
Member

Choose a reason for hiding this comment

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

OK, I read the code now, so I understand, but I think an example would be great. Basically just saying: we start out with an iterator over 0..usize::max, then split this range in half, and so forth.

Also, should we use u64 just to get that much more resolution?

Also, I think we need a test for the case where we have an iterator that exceeds the resolution of the false indices. Or at least some careful comments explaining why it is correct. =)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, I'll work on that explanation.

I think AtomicU64 is not stable yet (tracked here). Would it be worth postponing merging this PR until it's stable? Of course, we should be able to make that change later, because the exact size of the integer used is private to this module.

I agree that a test would be useful to validate my hand-waving in the comments. Any thoughts on how to write that test? I don't yet know enough about the splitting logic to know how to force that case. One possibility is to start with a FindConsumer whose upper and lower bounds are the same, but that requires either making the FindConsumer struct public so it can be used in the test module, or writing a test directly in this module (which none of the rest of this project does).

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 AtomicU64 is not stable yet (tracked here). Would it be worth postponing merging this PR until it's stable? Of course, we should be able to make that change later, because the exact size of the integer used is private to this module.

Nah, just use a usize. It'll make it easier to write a test. :)

As for the test... well, you could... perhaps write something that drives the splitting by hand? i.e., take a u64 range, create a producer from it, and manually call split etc? The test can be internal to this module if it needs access to private state...

...at minimum some thorough comments for how that case is handled would be a good start.

// divide the range in half
let old_lower_bound = self.lower_bound.get();
let median = old_lower_bound + ((self.upper_bound - old_lower_bound) / 2);
self.lower_bound.set(median);
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps we can rename split_off() to split_left() or split_off_left() or split_left_off(), something like that.

// before, depending on timing. This means more consumers will
// continue to run than necessary, but the reducer will still ensure
// the correct value is returned.
self.best_found.swap(self.boundary, Ordering::Relaxed);
Copy link
Member

Choose a reason for hiding this comment

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

Yes, seems like it'd be worth using a compare-exchange here. I'd probably use compare_exchange_weak, but that's a minor thing.

fn full(&self) -> bool {
let best_found = self.best_found.load(Ordering::Relaxed);
match self.match_position {
MatchPosition::Leftmost => best_found < self.boundary,
Copy link
Member

Choose a reason for hiding this comment

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

Wait, am I confused? Why is self.item() sufficient? Don't we want to also stop if some other folder (to our left) has found an item?

where FIND_OP: Fn(&Self::Item) -> bool + Sync {
find_first_last::find_last(self, predicate)
}

#[doc(hidden)]
#[deprecated(note = "parallel `find` does not search in order -- use `find_any`")]
Copy link
Member

Choose a reason for hiding this comment

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

let's update this deprecated note to say: "use find_any, find_first, or find_last" -- we might also consider calling find_first(), not sure.

@nikomatsakis
Copy link
Member

This is nice =)

@schuster
Copy link
Contributor Author

Just pushed with updates to address all feedback.

@schuster
Copy link
Contributor Author

Actually, I just found a bug in the range calculation. Stand by for a fix.

@schuster
Copy link
Contributor Author

Fixed. Turned out to be a problem with the test itself, not the code.

Copy link
Member

@nikomatsakis nikomatsakis left a comment

Choose a reason for hiding this comment

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

These changes look great! I left a few nits.

// (i.e. its length is 1), in which case the split returns two consumers with
// the same range. In that case both consumers will continue to consume all
// their data regardless of whether a better match is found, but the reducer
// will still return the correct answer.
Copy link
Member

Choose a reason for hiding this comment

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

Great comment =) Very clear.

&first_found);

// split until we have an indivisible range
let bits_in_usize = usize::min_value().count_zeros();
Copy link
Member

Choose a reason for hiding this comment

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

cute

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was actually the most direct way I could find to get the number of bits in a usize, believe it or not. Is there some other way that I missed?

let right_first_folder = right_first_folder.consume(2).consume(3);
assert_eq!(first_reducer.reduce(left_first_folder.complete(),
right_first_folder.complete()),
Some(0));
Copy link
Member

Choose a reason for hiding this comment

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

one nit -- while we are testing all this, maybe we can keep around also the consumer where we expect full() to be true? For example:

for i in 0..bits_in_usize - 1 {
    first_consumer.split_off();
}

let second_folder = first_consumer.split_off().into_folder();
...
assert!(!right_first_folder.full()); 
assert!(second_folder.full());

The name second_folder is kind of weak though...but whatever =)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you mean that you want to see a consumer from outside of the exhausted range, so that we can check that it's full when the consumer from inside the range finds a match? That wasn't quite what your example code does (you'd have to move the creation of second_folder above the loop, or something like that), but I think that's what you're going for.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, that's what I meant.

Some(0));

// same test, but for find_last
let last_found = AtomicUsize::new(0);
Copy link
Member

Choose a reason for hiding this comment

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

can you break this into a distinct #[test]?

Copy link
Member

Choose a reason for hiding this comment

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

also, as a style nit, at least in rayon I would tend to convert this into a directory module (i.e., find_first_last/mod.rs and put the tests in find_first_last/test.rs). But .... I don't really care about this. Maybe just move them to the end of the file or something.

(I would prefer not to have an inline mod test { ... } module, as many people prefer. I find managing the imports more annoying that way. I'm idiosyncratic like that I guess.)

self.boundary,
Ordering::Relaxed,
Ordering::Relaxed);
if exchange_result.is_err() {
Copy link
Member

Choose a reason for hiding this comment

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

Actually, compare_exchange_weak gives you back the value that you failed to swap in its Err variant. So you could rewrite this as:

let mut current = self.best_found.load(Ordering::Relaxed);
loop {
    if better_position(current, self.boundary, self.match_position) {
        break;
    }
    match self.best_found.compare_exchange_weak(current, self.boundary, Relaxed, Relaxed) {
        Ok(_) => {
            self.item = Some(item);
            break;
        }
        Err(v) => current = v,
    }
}

This seems mildly clearer (and more efficient) to me (I also prefer the loop+break formulation over the while loop; it's a nit, but I find it easier for me to parse what is happening).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed, I also find your formulation easier to understand.

@nikomatsakis
Copy link
Member

So one question is whether we should rename split_off() in this PR to reflect its new role. We can probably do it in a later PR.

@schuster
Copy link
Contributor Author

A later PR makes sense to me, too - it's a logically separate change. I could submit that PR as soon as this one goes through.

@nikomatsakis nikomatsakis merged commit 4614458 into rayon-rs:master Dec 31, 2016
@nikomatsakis
Copy link
Member

Looks great thanks @schuster !

schuster added a commit to schuster/rayon that referenced this pull request Jan 2, 2017
schuster added a commit to schuster/rayon that referenced this pull request Jan 3, 2017
nikomatsakis added a commit that referenced this pull request Jan 3, 2017
Rename split_off to split_off_left (as discussed in PR #189)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants