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

added by_blocks method #831

Merged
merged 11 commits into from Feb 27, 2024
Merged

added by_blocks method #831

merged 11 commits into from Feb 27, 2024

Conversation

wagnerf42
Copy link
Contributor

hi,

here is the code for blocks. in draft request like we discussed last time.

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.

I think this is a nice and powerful abstraction, but I also think we could make it easier for cases we expect to be common. Perhaps there could be a couple helpers like:

    // double each time, like your example, but this name is so-so
    fn by_doubling_blocks(self, first: usize) -> ByDoublingBlocks<I>; 

    // always use the same size, like `by_blocks(repeat(size))`
    fn by_uniform_blocks(self, size: usize) -> ByUniformBlocks<I>;

Those ideas could still use the same BlocksCallback internally.

// now we loop on each block size
while remaining_len > 0 && !consumer.full() {
// we compute the next block's size
let size = self.sizes.next().unwrap_or(std::usize::MAX);
Copy link
Member

Choose a reason for hiding this comment

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

There's a design choice here when sizes.next() returns None, with at least three options:

  • Consume the remainder as one entire block, which is what you have now.
  • Run the remainder for side effects, without feeding it into our consumer. Skip does this with its skipped prefix to match the semantics of std::iter::Skip.
  • Ignore the remainder altogether, basically acting like the suffix of Take.

Did you consider this? I think behavior like Take might actually be a nice choice.

Another question is whether size == 0 should be allowed. At the very least, it would be a wasted split here, which could just be considered the user's poor choice. Or we could make that impossible by having S produce NonZeroUsize items, but that makes it more annoying to write the iterator.

Copy link
Member

Choose a reason for hiding this comment

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

Hmm. It seems quite surprising to me to ignore some of the elements. I would definitely not expect that. Ultimately, I think we want to offer users a choice of how to manage this -- another behavior I could imagine would be "go on using the last block size that got returned until we are complete".

I am wondering if we can express these semantics with some composed operators? I'd sort of like to say something like .by_blocks().skip_remainder() or something. We could just add inherent methods to the return type to allow users to change back and forth between those modes, no?

Copy link
Member

Choose a reason for hiding this comment

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

It seems to me that the user can express most of those semantics in the iterator itself:

  • current MAX behavior: my_iter.chain(iter::once(usize::MAX))
  • repeat behavior: my_iter.chain(iter::repeat(last))
  • my suggested Take behavior: just my_iter, and let its None be the end
  • run remainder without consuming: ???

We could just add inherent methods to the return type to allow users to change back and forth between those modes, no?

Yeah, that's possible too.

Copy link
Member

Choose a reason for hiding this comment

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

another might be asserting that there is no remainder

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 at minimum we should document these techniques, and i might prefer a convenient way to express them

Copy link
Contributor Author

Choose a reason for hiding this comment

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

if we only allow constant sizes and doubling sizes in the public interface then this choice about what to do when the size iterator ends is of private concern.
i used to have both versions, one like an implicit take when it runs out and the other one forcing to consume everything. so there is indeed something to choose here.

i would favor consuming it all since I happened to get some bugs missing some elements.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

also, i have some questions like:

  • should i add unit tests ? where ?
  • should i add benches ? where ?

Copy link
Member

Choose a reason for hiding this comment

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

I would be in favor of only allowing the two options until we know there is demand for something more general. As for unit tests, we typically just add #[test] in the modules, I think? Alternatively, you can add tests into the tests directory but we don't usually do that. (Is this right, @cuviper?)

For benchmarks, we generally modify the rayon-bench project.

Copy link
Member

Choose a reason for hiding this comment

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

For tests, there are a few generic sanity checks to add: tests/clones.rs, tests/debug.rs, and src/compile_fail/must_use.rs. Beyond that, adding unit #[test] directly in the module is fine, or break into a tests submodule if there are a lot. Doc-test examples are also nice to serve both doc and testing roles.

/// we stop at the first block containing the searched data.
fn by_blocks<S: IntoIterator<Item = usize>>(self, sizes: S) -> ByBlocks<Self, S> {
ByBlocks::new(self, sizes)
}
/// Collects the results of the iterator into the specified
Copy link
Member

Choose a reason for hiding this comment

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

tiny nit: please add a blank line between items like the two functions here, and also between the type and fn in your trait implementations.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sorry i just saw this comment. in the meantime i changed the file so i'm not too sure what you are referring to.

Copy link
Member

Choose a reason for hiding this comment

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

I'll add inline suggestions for it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok this is taking shape.

however there are some functions i had which are not included here.
i'm not too sure if we should add them at least in this pull request.

@wagnerf42
Copy link
Contributor Author

I think this is a nice and powerful abstraction, but I also think we could make it easier for cases we expect to be common. Perhaps there could be a couple helpers like:

    // double each time, like your example, but this name is so-so
    fn by_doubling_blocks(self, first: usize) -> ByDoublingBlocks<I>; 

    // always use the same size, like `by_blocks(repeat(size))`
    fn by_uniform_blocks(self, size: usize) -> ByUniformBlocks<I>;

Those ideas could still use the same BlocksCallback internally.

hi, sorry for the delay. that's a good idea. it's the only two patterns i was able to come up with for now.

@wagnerf42
Copy link
Contributor Author

hi, so I did the changes to by_doubling_blocks and by_uniform_blocks.

i also removed the initial block size argument in by_doubling_blocks to make it more transparent to the user. tell me if you think it is not a good idea.

quick questions:

  • is it ok to return in both cases a ByBlocks or should I get two different types and hide the size iterator ?
  • is it ok if i do modifications to rayon_demo/src/find ? find_last and find_first are calling find_any

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.

These are the style nits I mentioned, just adding blank lines.

Comment on lines 42 to 17
type Output = C::Result;
fn callback<P: Producer<Item = T>>(mut self, mut producer: P) -> Self::Output {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
type Output = C::Result;
fn callback<P: Producer<Item = T>>(mut self, mut producer: P) -> Self::Output {
type Output = C::Result;
fn callback<P: Producer<Item = T>>(mut self, mut producer: P) -> Self::Output {

Comment on lines 79 to 116
type Item = I::Item;
fn drive_unindexed<C>(self, consumer: C) -> C::Result
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
type Item = I::Item;
fn drive_unindexed<C>(self, consumer: C) -> C::Result
type Item = I::Item;
fn drive_unindexed<C>(self, consumer: C) -> C::Result

src/iter/mod.rs Outdated
Comment on lines 2216 to 2491
}
/// Normally, parallel iterators are recursively divided into tasks in parallel.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
}
/// Normally, parallel iterators are recursively divided into tasks in parallel.
}
/// Normally, parallel iterators are recursively divided into tasks in parallel.

src/iter/mod.rs Outdated
Comment on lines 2235 to 2516
}
/// Collects the results of the iterator into the specified
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
}
/// Collects the results of the iterator into the specified
}
/// Collects the results of the iterator into the specified

@cuviper
Copy link
Member

cuviper commented Mar 29, 2021

  • is it ok to return in both cases a ByBlocks or should I get two different types and hide the size iterator ?

I think it would be better to hide those details.

  • is it ok if i do modifications to rayon_demo/src/find ? find_last and find_first are calling find_any

Yes, that's fine -- it's probably a good place to showcase the difference between these approaches.

src/iter/mod.rs Outdated
///
/// This can have many applications but the most notable ones are:
/// - better performances with [`find_first()`]
/// - more predictable performances with [`find_any()`] or any interruptible computation
Copy link
Member

Choose a reason for hiding this comment

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

This is good -- I think it'd be good to make a section like

# When to use this method

and put this text in there, and maybe point at by_uniform_blocks as an alternative. I think users will be unclear when to sue it.

/// This adaptor changes the default behavior by splitting the iterator into a **sequence**
/// of parallel iterators of given `blocks_size`.
/// The main application is to obtain better
/// memory locality (especially if the reduce operation re-use folded data).
Copy link
Member

Choose a reason for hiding this comment

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

This is good -- I think it'd be good to make a section like

# When to use this method

and put this text in there, and maybe point at by_uniform_blocks as an alternative. I think users will be unclear when to use it.

@wagnerf42
Copy link
Contributor Author

some more things which can be done:

  • add a sequential fold or try_fold method as argument to by_blocks (well, in a new method).
    this way we could try_fold sequentially the results reduced in each block.
    it would actually be needed to have the best manual find_first implementation.
  • have another iterator size based on timings (like: one block every x duration)

i'm a bit worried about the combinations between block sizes and iterator choices because
we would have 2 or 3 iterator choices times 2-3 block sizes choices so that's up to 9 methods.

@wagnerf42
Copy link
Contributor Author

actually it would not work this way because we would need to know what the return type is and this info is not known until the call to reduce. i think it might still be possible to do it but i would need to modify the Folder trait. if we add a seq_consume for consuming the blocks results then i think we can add a seq_fold method in ParallelIterator registering in the consumer what the blocks folding method would be.

i'll give it a try

#[must_use = "iterator adaptors are lazy and do nothing unless consumed"]
#[derive(Debug, Clone)]
pub struct ExponentialBlocks<I>(
ByBlocks<I, std::iter::Successors<usize, fn(&usize) -> Option<usize>>>,
Copy link
Member

Choose a reason for hiding this comment

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

This only needs to store I, and then a ByBlocks can be created on the fly in drive_unindexed. That will also let it use the direct unnameable closure type, rather than forcing it to a function pointer.

/// [`IndexedParallelIterator`]: trait.IndexedParallelIterator.html
#[must_use = "iterator adaptors are lazy and do nothing unless consumed"]
#[derive(Debug, Clone)]
pub struct UniformBlocks<I>(ByBlocks<I, std::iter::Repeat<usize>>);
Copy link
Member

Choose a reason for hiding this comment

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

Similarly, this really only needs to store I and blocks_size at first, but in this case it probably doesn't matter either way.

@cuviper
Copy link
Member

cuviper commented Feb 10, 2024

@wagnerf42 did you ever try the suggestion I had about dropping the I parameters? Essentially making them lazier about that, IIRC. I also wonder if you were still exploring your own ideas, since this is still in Draft.

@wagnerf42
Copy link
Contributor Author

@wagnerf42 did you ever try the suggestion I had about dropping the I parameters? Essentially making them lazier about that, IIRC. I also wonder if you were still exploring your own ideas, since this is still in Draft.

well, i'm not too sure about what you mean. sorry it's been a while ago.
this adapter is one i use quite often and i'd rather get it done right.

wagnerf42 and others added 10 commits February 12, 2024 20:00
    - ByBlock is now private
    - indentation fixes
we could win big by adding more operations.
it is super nice to have the sequential iterator on all reduced values
of blocks.
we could then use the dumb_find on each block and use the sequential
find to find the first value.

this would be the best algorithm.

sadly we can't have it due to rayon's types encapsulation.
the next best thing would be a
try_fold_by_exponential_blocks method
it would take a closure on Self producing the reduced values
and a closure for the sequential try_fold
@cuviper cuviper marked this pull request as ready for review February 13, 2024 05:19
@cuviper
Copy link
Member

cuviper commented Feb 15, 2024

If you have any comments on what I added, please let me know!

I plan to publish a new release soon, and it would be nice to include this.

@cuviper cuviper added this pull request to the merge queue Feb 27, 2024
Merged via the queue into rayon-rs:main with commit bacd468 Feb 27, 2024
4 checks passed
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.

None yet

3 participants