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

impl IntoParallelIterator for Range<char> #771

Merged
merged 6 commits into from
Jul 21, 2020
Merged

Conversation

CAD97
Copy link
Contributor

@CAD97 CAD97 commented Jun 18, 2020

Closes #770

Implements both unindexed and indexed parallel iteration for Range<char> and RangeInclusive<char>.

@CAD97 CAD97 marked this pull request as draft June 18, 2020 05:15
@cuviper
Copy link
Member

cuviper commented Jun 18, 2020

I'm not so concerned about 16-bit usize for rayon -- the only such target built into rust is msp430-none-elf, which has core only.

src/range.rs Outdated Show resolved Hide resolved
@CAD97
Copy link
Contributor Author

CAD97 commented Jun 18, 2020

I've updated the PR to just be for Range<char> currently, which works already. The tests only fail because I added a smoke test for correctness across the surrogate range that tests against the std impl, which isn't stable until 1.45.

RangeInclusive cannot be implemented in the same manner, as RangeInclusive<u32> is not ExactSizeIterator, so the chain isn't IndexedParallelIterator. (I'm working on doing so only using Range<u32> right now.)

@cuviper
Copy link
Member

cuviper commented Jun 18, 2020

RangeInclusive cannot be implemented in the same manner, as RangeInclusive<u32> is not ExactSizeIterator, so the chain isn't IndexedParallelIterator. (I'm working on doing so only using Range<u32> right now.)

Yeah, end_char as u32 + 1 will never overflow, so Range<u32> should be fine.

@CAD97 CAD97 marked this pull request as ready for review June 18, 2020 21:35
@CAD97
Copy link
Contributor Author

CAD97 commented Jun 18, 2020

Alright, this should be correctly functional now. (Just that the tests require rustc 1.45 which is currently beta. (I can fix this by just comparing against a constant test case.))

@CAD97
Copy link
Contributor Author

CAD97 commented Jun 18, 2020

The tests before 56ef498 are more obviously correct (as they compare the rayon and std implementations), but said commit compares them against a provided vector...

but the impl requires RangeInclusive<char>: Iterator anyway so that we can get the bounds and whether the iterator is exhausted.

So RangeInclusive<char>: IntoParallelIterator seems to need to be gated behind RangeInclusive<char>: Iterator, unlike the half-open case.

where
C: Consumer<Self::Item>,
{
if let Some((start, end)) = self.bounds() {
Copy link
Member

Choose a reason for hiding this comment

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

Hmm, bounds() won't work until Rust 1.45...

FWIW, I was also looking at that function in godbolt, and it works a lot better for char this way:

    if range.size_hint().0 > 0 {
        Some((*range.start(), *range.end()))
    } else {
        None
    }

https://rust.godbolt.org/z/8FrRRW

But it does require that we rely on size_hint() to be correct (like TrustedLen). I guess this code shouldn't be a hotspot anyway, so it probably doesn't matter.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The correct way to do it would be to use .is_empty() if/when it stabilizes.

That can be optimized in a followup if necessary/desired, I suppose. I don't think it would be incorrect to use size_hint here, so long as there's a note about relying on TrustedLen-like semantics for correctness (but hopefully not safety?).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Side note: oof, there's a unwrap/expect panic still in there. I guess I need to go tweak the impls again to see if I can remove that (as it should be completely unreachable, and if it isn't, that's definitely a bug.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, the generated asm is a lot better when I add in the missed _unchecked to next_back: https://rust.godbolt.org/z/9dviLq

We already have it for next because I spotted the missed optimization out of the unreachable panicking code, but for some reason I didn't think to check reverse iteration for the same thing.

Copy link
Member

Choose a reason for hiding this comment

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

I don't think it would be incorrect to use size_hint here, so long as there's a note about relying on TrustedLen-like semantics for correctness (but hopefully not safety?).

There's no safety problem, just start() and end() are unspecified when empty.

@cuviper
Copy link
Member

cuviper commented Jun 18, 2020

So RangeInclusive<char>: IntoParallelIterator seems to need to be gated behind RangeInclusive<char>: Iterator, unlike the half-open case.

You can add a test to the build script to get a cfg.

@CAD97
Copy link
Contributor Author

CAD97 commented Jun 18, 2020

You can add a test to the build script to get a cfg.

I got that in before you suggested it 🙃

Manually tested that this does engage and work on 1.45.

@cuviper
Copy link
Member

cuviper commented Jun 18, 2020

For the other RangeInclusive impls we have a convert! macro which hides the code duplication -- maybe you could add a convert_char! for your case? You only have two places each, but it's still fairly complicated code.

@CAD97
Copy link
Contributor Author

CAD97 commented Jun 18, 2020

Added a macro to deduplicate; I think this is what you were suggesting?

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 it's good, but I'd like to wait for working Range<char> to actually reach stable, especially since we're not guarding the implementation of rayon::range::Iter<char>.

@cuviper
Copy link
Member

cuviper commented Jun 18, 2020

And since I can't help tinkering, here's another option for bounds without Iterator:

    let start = *range.start();
    let end = *range.end();
    if start <= end && (start..=end) == *range {
        Some((start, end))
    } else {
        None
    }

... assuming Eq captures the private exhausted state, I think this would be OK. It actually looks better for char than for i32! https://rust.godbolt.org/z/TT2DBm

@CAD97
Copy link
Contributor Author

CAD97 commented Jun 19, 2020

And since I can't help tinkering, here's another option for bounds without Iterator:

Here's a comparison of a bunch of the proposed options, with next_back fixed to optimize out the unreachable overflow check: https://rust.godbolt.org/z/F25_Ej

TL;DR: it looks to my untrained eyes that any of the proposed options are roughly equivalent. (Note for _eq2: Some no longer implies there is iteration remaining, but could construct "reverse ranges" when given a reverse range. This will, however, still produce a correctly empty iterator.) I believe the char case looks better because of how niches interact with the return value (and thus the difference should disappear when inlined).

@CAD97
Copy link
Contributor Author

CAD97 commented Jun 19, 2020

The commit just pushed uses start <= end && range == (start..=end) to check that using the bounds is still correct. (The start <= end check, though redundant for the char case, is in fact requisite for the .chain(once(end)) case. This wasn't caught by tests when I implemented it -- needed test? u32::MAX..=0 would yield u32::MAX without the start <= end check.)

EDIT: no, I need to go to bed, apparently. The code is definitely still correct with bounds returning (high, low)... because if low is the maximum value, then it's >= the high value, so the range is not exhausted.

So bounds() could be implemented with just the range == (start..=end) check, but I'll keep the start <= end check to avoid changing its semantics in this PR.


Waiting until iterable Range<char> is stable to merge this makes sense.

bors added a commit to rust-lang-ci/rust that referenced this pull request Jul 14, 2020
…nieu

Use step_unchecked more liberally in range iter impls

Without these `_unchecked`, these operations on iterators of `char` fail to optimize out the unreachable panicking condition on overflow.

cc @cuviper rayon-rs/rayon#771 where this was discovered.
@cuviper
Copy link
Member

cuviper commented Jul 21, 2020

I think we're good for this now, thanks!

bors r+

@bors bors bot merged commit 97b7e34 into rayon-rs:master Jul 21, 2020
@CAD97 CAD97 deleted the range-char branch July 22, 2020 00:02
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.

par_iter for Range*<char>
2 participants