-
Notifications
You must be signed in to change notification settings - Fork 13k
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
.mod_euc()
produces invalid ouput for small negative floats due to rounding errors
#50179
Comments
It is currently implemented as pub fn mod_euc(self, rhs: f64) -> f64 {
let r = self % rhs;
if r < 0.0 {
r + rhs.abs()
} else {
r
}
} https://github.com/rust-lang/rust/blob/master/src/libstd/f64.rs#L236 What happens is that because A more stable implementation would be, I believe, for example something like pub fn mod_euc(self, rhs: f64) -> f64 {
((self % rhs) + rhs.abs()) % rhs
} |
.mod_euc()
produces invalid ouput for small negative floats.mod_euc()
produces invalid ouput for small negative floats
Thanks @fkjogu for the investigation, and good catch! pub fn mod_euc(self, rhs: f64) -> f64 {
let mut r = self % rhs;
if r < 0.0 {
r += rhs.abs();
if r == rhs {
return 0.0;
}
}
r
} would be a slightly better implementation, as it avoids the extra unnecessary division from the second Correspondingly, I imagine |
This is also how it behaves in Python: >>> -1e-18 % 1
1.0 and in Haskell: λ> import Data.Fixed
λ> (-1e-18) `mod'` 1.0
1.0 For better or worse, it seems that virtually all possible implementations of floored or Euclidean modulus of floating point numbers naturally have this problem unless you explicitly account for this nasty edge case. |
To play devil's advocate though, a colleague of mine has shared his story of the worst bug he ever had to debug, which was basically caused by having exactly this happen in his own handwritten But that was in C++, where UB reigns supreme. |
That's very interesting; I wasn't aware of that. Regardless, this bug should definitely be fixed. I'd feel more comfortable addressing it along with a fix to |
True, one could argue that this is just how floating point arithmetic works. Floats certainly break many axioms, for example the uniqueness of the neutral element in the additive group: I'll admit that I do not give a strong argument. Maybe I just got used to only a subset of floating point oddities 😉. Edit: Or to argue from another side: I'm okay for an operator like |
My thoughts exactly. The whole point of the method is to be the "good" version of |
I think that setting this precedent is something I can get behind; just be aware there is another closely-related edge case at pub fn mod_euc(self, rhs: f64) -> f64 {
let mut r = self % rhs;
if r < 0.0 {
r += rhs.abs();
- if r == rhs {
+ if r == rhs.abs() {
return 0.0;
}
}
r
} P.S. bear in mind the ultimate property of any a == a.div_euc(b) * b + a.mod_euc(b) I just tested with Python's (flooring) division and float, and see that it generally satisfies this property for pairs of numbers uniformly drawn at random from import sys
import random
def uniform(amplitude):
return amplitude * (2 * random.random() - 1)
amp_1 = float(sys.argv[1])
amp_2 = float(sys.argv[2])
while True:
a = uniform(amp_1)
b = uniform(amp_2)
if a != (a // b) * b + (a % b):
print(a, b)
sys.exit(1) $ python3 find-counterexample.py 1.0 1.0
(CPU spins up)
(time passes.........)
(okay, I'm bored)
^C
$ python3 find-counterexample.py 1.0 1e15
-0.7833003032084154 281330340541134.03
$ python3 find-counterexample.py 1e15 1.0
556555443188291.3 -0.047383938082182775 I wonder how well the corrected definitions fare at this? (my guess: probably still not well, and it might not even be possible to produce outputs satisfying this property for certain inputs. Not sure.) |
I'd like to contribute a different perspective: I consider the current behavior to be reasonable and correct but think we should document that the return value of Let me explain why I think so: Normally, the way floating point operation work (at least the basic operations: addition, subtraction, multiplication and division) is that they produce the floating point number that is closest to the mathematically correct result. For example, Applying this to the example In addition, if we wanted to preserve the (desirable) property that But my main point is that deviating from the principle "floating point results should be as close as possible to the mathematically exact result" requires a really good reason. I don't see why the requirement
I agree. I just think that the current version is the "best" version of |
I was about to argue against this, but after thinking about it, you're right. Basically, I was going to say that the distance to the correct result should be measured in "periodic space;" in this sense, 0.0 is "just as correct" as 3.0, because in the quotient space But then I realized the flaw in this argument. We can't measure error in periodic space, because otherwise the best implementations would clearly be the following: fn div_euc(a: f64, b: f64) -> f64 { 0 }
fn mod_euc(a: f64, b: f64) -> f64 { a } so our definition of error needs to be one that can differentiate between I am just sharing in case somebody else were to try to make this argument. |
I disagree: we should measure the error in the periodic range
We should have I agree we also want |
@varkor If our highest priority is that I just think it's okay to weaken that requirement for floating points because they are not precise by their very nature. To give an analogy: Mathematically, we have
So here we definitely have a problem because in the example above, we have to set |
I find @fanzier rather convincing. It is true, we cannot fix all edge cases floats introduce. But no matter the outfall, in my opinion the discussed property should be at least documented, because I find it surprising. The reason it surprises me more than in the example So my conclusion is, leave it as it is, but add some friendly documentation:
If you agree, I can create a pull request. |
I just realized the property #![feature(euclidean_division)]
fn main() {
let m = 3.0f64;
let f = m - std::f64::EPSILON;
let q = f.div_euc(m);
let r = f.mod_euc(m);
assert_eq!(f, m * r + q);
} https://play.rust-lang.org/?gist=2a2664c0c1d80746cc3c00518499cfbd&version=nightly It's like playing Whac-A-Mole. |
Definitely, I also said that in my first comment :)
I think we should specify that we mean
No, it is not. (At least not in your example.) Note that |
Perhaps it would help to examine the use cases this method is meant to address. If it is supposed to be "as close as possible" (as one usually wants for mathematical functions), it seems to me that sometimes the correctly rounded result will be rounded up to the divisor in the natural implementation (although one could make an argument that 0 is an equally correct result, it's not more correct). If, on the other hand, this method is intended to bring an input into a half-open range (e.g. because you're computing a function that is not defined everywhere, or has a discontinuity you'd rather avoid) then the result should clearly be strictly less than the divisor. |
I am very sorry, i had a bug in my code (or rather my brain). #![feature(euclidean_division)]
fn main() {
let m = 3.0f64;
let f = m - std::f64::EPSILON;
let q = f.div_euc(m);
let r = f.mod_euc(m);
- assert_eq!(f, m * r + q);
+ assert_eq!(f, m * q + r);
} Indeed it yields the correct result. |
.mod_euc()
produces invalid ouput for small negative floats.mod_euc()
produces invalid ouput for small negative floats due to rounding errors
I have created a pull-request. Feel free to shout out any improvements. |
Document round-off error in `.mod_euc()`-method, see issue rust-lang#50179 Due to a round-off error the method `.mod_euc()` of both `f32` and `f64` can produce mathematical invalid outputs. If `self` in magnitude is much small than the modulus `rhs` and negative, `self + rhs` in the first branch cannot be represented in the given precision and results into `rhs`. In the mathematical strict sense, this breaks the definition. But given the limitation of floating point arithmetic it can be thought of the closest representable value to the true result, although it is not strictly in the domain `[0.0, rhs)` of the function. It is rather the left side asymptotical limit. It would be desirable that it produces the mathematical more sound approximation of `0.0`, the right side asymptotical limit. But this breaks the property, that `self == self.div_euc(rhs) * rhs + a.mod_euc(rhs)`. The discussion in issue rust-lang#50179 did not find an satisfying conclusion to which property is deemed more important. But at least we can document the behaviour. Which this pull request does.
Rollup of 7 pull requests Successful merges: - #49987 (Add str::split_ascii_whitespace.) - #50342 (Document round-off error in `.mod_euc()`-method, see issue #50179) - #51658 (Only do sanity check with debug assertions on) - #51799 (Lower case some feature gate error messages) - #51800 (Add a compiletest header for edition) - #51824 (Fix the error reference for LocalKey::try_with) - #51842 (Document that Layout::from_size_align does not allow align=0) Failed merges: r? @ghost
As far as I'm aware, |
The newly added method
std::f64::mod_euc()
can produce values equal to the modulus when applied to a small negative float and sufficiently big modulus. AFAIK this should never happen.I tried this code on nightly
rustc 1.27.0-nightly (ac3c2288f 2018-04-18)
and the version on playground:https://play.rust-lang.org/?gist=01a506080183f14067553856de6763ab&version=nightly
I expected to see this happen:
The operation should produce only values in between
0. <= x < modulos
.Instead, this happened:
But it produced a value equal to the modulus, in this case
3.
. This is true for all sufficiently big floats relative to the machine epsilon.Discussion
It is likely a bug in the implementation ignoring some floating point arithmetic oddities. I haven't looked deeper yet.
The text was updated successfully, but these errors were encountered: