-
Notifications
You must be signed in to change notification settings - Fork 142
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
Right shift and division with rounding. #203
Conversation
Fairly correct implementation of right-shift with rounding. Also fairly efficient, though I'm sure I'll find some cases where we're generating uncessary overflow checks upon further inspection. Nonetheless, a decent enough start.
|
@swift-ci test |
|
@swift-ci test |
11add40
to
cddd5e6
Compare
|
@swift-ci test |
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.
Reviewed API only, didn't look at the implementation at all
|
|
||
| /// A rule that defines how to select one of the two representable results | ||
| /// closest to a given value. | ||
| public enum RoundingRule { |
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 feels a little inconsistent.
…, rounding: .down ✅
..., rounding: .stochastic should this be "stochastically"?
..., rounding: .trap in what circumstances does it trap? (I realize this is clear if you think about it for a moment, but maybe we can make it obvious at the point of use?)
Related, do we want rounding or do we want rounded? With the name shifted we're describing the resulting value, rather than the action, but with rounding we're describing the action.
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.
Yeah, I'm not super-delighted with "trap" either. Still trying to come up with a better name for it. In the meantime, it has the virtue of being short, and strongly suggesting that it can trap, so that's not a surprise.
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.
trapIfUnrepresentable?
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.
.exactOrTrap maybe?
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.
Maybe just .exact, though I would like to mention "trap" in the name. I like the symmetry of exact with the exactly: inits from the stdlib.
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 like .exactOrTrap or something similar. "Trap" is kind of a specialist word, though, so maybe .exactOrFatal? .exactOrAssert? .mustBeExact? I think it makes sense to have this one be linguistically distinct, so .onlyExact or .mustBeExact or .remainderMustBeZero or ...
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 this was implemented in the standard library, would the new cases be added to FloatingPointRoundingRule, or should the new enum be renamed to IntegerRoundingRule instead?
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 like to deprecate FloatingPointRoundingRule eventually and replace it with RoundingRule. Exactly what that migration path looks like isn't totally clear, but I think it's the direction that we'll take if/when these show up in the stdlib.
The existing FloatingPointRoundingRule has a few problems:
- it's an open enum, which has some not-ideal representation/performance tradeoffs for Swift. It should have been a frozen struct with static vars instead (so the layout could be frozen while still being able to add values), but that pattern wasn't yet widely used when the type was added.
- rounding rules generally make sense for integers as well as floating-point (see this API). Even if a given rule is more/less efficient to implement or more/less useful for one type or another, it's nice to have a uniform set.
Sources/IntegerUtilities/Shift.swift
Outdated
| /// 5.shifted(right: 1, rounding: .trap) | ||
| @inlinable | ||
| public func shifted<Count: BinaryInteger>( | ||
| right count: Count, |
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 forget, what's the precedent here: do we usually include or omit "By"?
Sources/IntegerUtilities/Shift.swift
Outdated
| @inlinable | ||
| public func shifted<Count: BinaryInteger>( | ||
| right count: Count, | ||
| rounding rule: RoundingRule = .down |
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.
As someone who doesn't think about rounding very often I'm just going to assume that you've thought about this and there's an obvious default here, but I'm leaving this comment just on the tiny chance that on seeing it again you'll go "hm actually maybe they should always have to say what they want" :)
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 right shift, the default should certainly be to truncate. That's down or towardsZero for positive values. I have to stop and think for a bit before I can remember the right behavior for negative values.
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.
Right; "normal" right shifts (>>) round down, and there's no good reason to make anything else the default. "Normal" division (/) rounds toward zero, which is why divide by power of two can't be replaced with a shift for signed values, but the way to resolve that mismatch is to make the forthcoming divide-with-rounding default to .down as well, as that gives a more sensible remainder for most uses.
| /// | ||
| /// This is the default rounding mode for integer shifts, including the | ||
| /// shift operators defined in the standard library. | ||
| case down |
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.
What do you think about additionally providing something like:
static var `default`: Self { .down }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 the main concern would be that we might add an operation someday that wants to use a different default. round-to-nearest multiple comes to mind as a possibility.
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.
Ah, so default depends on operation (so either current design or name it defaultShifting, etc.). What about a "fastest" or "I don't care" computed var, would that also be dependent on the operation?
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.
"don't care" here would just be >>, and for divide it would be /. Expectation is that people reach for these when they do care.
Sources/IntegerUtilities/Shift.swift
Outdated
| // have to take the RNG in-out. The same problem applies to rounding | ||
| // with dithering. We should consider adding a stateful rounding API |
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.
Is mentioning dithering relevant? I find it confusing, especially since I thought that it is exactly this case.
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 // comments are mostly notes for myself, rather than public documentation. =) Referring to error-diffusion or ordered dithering here, though that isn't obvious.
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'd include stochastic rounding in this API. It raises too many questions. (Ulichney's "Digital Halftoning" is a great introduction that discusses the importance of controlling the frequency distribution of your noise source.)
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.
Blue noise and the like and ordered dithers require a stateful rounding API, but even stochastic rounding without state is quite useful for some purposes.
Also implemented some basic statistical tests for .stochastic and reorganized code a little bit. The statistical justification of the thresholds for these tests should be explained a bit more, and we should decide on a policy in general for statistical tests that may fail sporadically, but these shouldn't be very noisy for now.
|
@swift-ci test |
2a9d2a1
to
501f582
Compare
|
@swift-ci test |
Also simplifies check logic for divide with rounding, which is a modest speedup for testing.
|
@swift-ci test |
|
@swift-ci test linux |
Fairly correct implementation of right-shift and division with rounding. Also fairly efficient, though I'm sure I'll find some cases where we're generating uncessary overflow checks upon further inspection. Nonetheless, a decent enough start.
The methods are added to BinaryInteger:
shifted<Count: BinaryInteger>(right: Count, rounding: RoundingRule) -> Selfdivided(by: Self, rounding: RoundingRule) -> SelfThe following methods are added to SignedInteger (not BinaryInteger, because the remainder is not generally representable in an UnsignedType when the rounding mode is not
.downor.towardZero):remainder(dividingBy: Self, rounding: RoundingRule) -> Selfdivded(by: Self, rounding: RoundingRule) -> (quotient: Self, remainder: Self)The following free function is added:
euclideanDivision<T: BinaryInteger>(_: T, _: T) -> (quotient: Self, remainder: Self)