-
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
added by_blocks method #831
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.
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); |
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 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 ofstd::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.
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 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?
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 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: justmy_iter
, and let itsNone
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.
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.
another might be asserting that there is no remainder
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 at minimum we should document these techniques, and i might prefer a convenient way to express 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.
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.
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.
also, i have some questions like:
- should i add unit tests ? where ?
- should i add benches ? where ?
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 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.
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.
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 |
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.
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.
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.
sorry i just saw this comment. in the meantime i changed the file so i'm not too sure what you are referring to.
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'll add inline suggestions for it.
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 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.
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. |
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:
|
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.
These are the style nits I mentioned, just adding blank lines.
src/iter/blocks.rs
Outdated
type Output = C::Result; | ||
fn callback<P: Producer<Item = T>>(mut self, mut producer: P) -> Self::Output { |
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.
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 { |
src/iter/blocks.rs
Outdated
type Item = I::Item; | ||
fn drive_unindexed<C>(self, consumer: C) -> C::Result |
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.
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
} | ||
/// Normally, parallel iterators are recursively divided into tasks in parallel. |
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.
} | |
/// 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
} | ||
/// Collects the results of the iterator into the specified |
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.
} | |
/// Collects the results of the iterator into the specified | |
} | |
/// Collects the results of the iterator into the specified |
I think it would be better to hide those details.
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 |
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 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). |
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 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.
some more things which can be done:
i'm a bit worried about the combinations between block sizes and iterator choices because |
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 i'll give it a try |
src/iter/blocks.rs
Outdated
#[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>>>, |
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 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.
src/iter/blocks.rs
Outdated
/// [`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>>); |
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.
Similarly, this really only needs to store I
and blocks_size
at first, but in this case it probably doesn't matter either way.
@wagnerf42 did you ever try the suggestion I had about dropping the |
well, i'm not too sure about what you mean. sorry it's been a while ago. |
- 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
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. |
hi,
here is the code for blocks. in draft request like we discussed last time.