How is the principle in Rust to compare two f32 numbers?

When the bits are the same, the real-or-infinite number they represent is the same, and they will compare equal. The only exceptions are -0 and +0, which are equal but have different bits, and NaN, which is false for every comparison to every number except !=.

Rust doesn't check if the two floats are close, only that they're exactly the same.

You've simply created two different numbers: 19.62 is different from 11.62 + 8.0. They aren't mathematically equivalent because addition of floats is not mathematically exact.

1 Like

I think no such thing.

What I think - and what I mean to explain to OP, here - is that the IEEE floating point spec doesn't constrain the implementation techniques used to produce results. OP was lead to ask whether floating-point equality entailed bitwise equality. For finite floating point values, the two are equivalent, but there's no guarantee as to whether Rust does or does not use bitwise comparison under the hood. You can't tell, unless you read the generated code for the target platform you're interested in. For certain non-finite values, equality is necessarily not bitwise equality, because the results the spec demands are inconsistent with bitwise comparison.

OP also asked whether infinities have any equality guarantees. They do, as it happens: all infinities of the same sign are equal, and all of them are either strictly greater than (positive) or strictly less than (negative) any finite value.

1 Like

Actually it's not 0.300000011920928955, it's 0.300000011920928955078125.

Ok. So, the comparison between two finite numbers is implementation-defined, right?

The mechanism is, yes.

The results of a comparison must have the properties the spec requires. That includes that two data representing equal numbers, compare equally, just to be clear. Where I think your surprise comes from is that the two expressions in your original post - 19.62 and 11.62 + 8.00, are not required by the spec to produce the same datum, even though by conventional arithmetic they are equal numbers.

What does "data representing equal numbers" mean here? Could you elaborate on the meaning here?

No it's not (except on certain platforms where Rust doesn't currently implement IEEE754, such as i586-unknown-linux-gnu, but that's basically considered a bug that will get fixed).

@derspiny is making confusing statements.

Floating-point representation is basically base-2 scientific notation with some finite precision (24 bits after the binary point for f32, 54 bits for f64). They are basically all rationals with the denominator being some (small or large) power of 2. When you want to work with "real numbers" in the mathematical sense, those can't always be exactly expressed using such rationals, so the set of floating-point numbers can only give a discrete approximation to the set of real numbers.

The base of 2 also doesn't have all prime factors in common with base 10 (= 2 * 5), so not all rational numbers represented as finite decimals can be exactly represented by finite binary notation. Therefore, interpreting certain (most, in fact) decimal literals in the source will necessarily involve approximation and rounding to the nearest representable float, as others have already explained it.

The converse is not true: every possible bit pattern of an IEEE-754 float corresponds to either:

  • NaN;
  • positive or negative infinity;
  • or a valid rational number, which is by extension also a valid real number, which is simply the result of looking at the scientific notation represented by the bits of the f32/f64. If, for instance, your float is 1.5 * 2^-4, then that's exactly the rational (and real) number 3/32 or 0.09375 (in decimal).

The comparisons are defined in the most obvious and sensible way possible: they just compare the mathematical values. If you ignore the details around NaNs and subnormals and infinities, floats really do behave just like normal rational numbers. There's no magic, no nothing.

The thing that most people ignore is that floats can't represent all reals or even all base-10 rationals, and that arithmetic operations implicitly perform rounding (because they simply have no other sensible choice). But comparison operations aren't weird and they don't perform any sort of further manipulations on their operands. They simply compare the represented values, period.

4 Likes

A floating-point datum is any value representable in a floating-point format, including a floating-point number, an infinity, or a NaN. Section 3 goes into detail of how floating point formats work, and thus what numbers can be represented - what numbers have a datum - in any given format.

None of this really adds much complexity to understanding comparison; if two floating point numbers represent distinct real numbers, then IEEE 794 (section 3, if you want a citation on this) requires that they also have distinct representations, and that they are distinct floating point data. If they're distinct, they must compare unequal to one another.

If comparing two finite numbers does not involve magic and two real numbers will compare equally if they represent the same value(from a mathematical perspective), why do we need to do something like that in Clippy Lints

let error_margin = f64::EPSILON; // Use an epsilon for comparison
// Or, if Rust <= 1.42, use `std::f64::EPSILON` constant instead.
// let error_margin = std::f64::EPSILON;
if (y - 1.23f64).abs() < error_margin { }
if (y - x).abs() > error_margin { }

What's the benefit here to doing this?

So, could I understand it as that: Aside from NAN and ZERO, if two f32 have the exact same bit sequences, then they represent the same real number?

Because of the parts that people ignore (most decimal representations have no exact encoding, operations round, ...), which lead to... your OP.

The lint is literally to point out why your OP code is misguided and did not produce the results you expected.

1 Like

That's because arithmetic operations still aren't exact. Or, to put it differently: floating-point numbers aren't mathematically closed over arithmetic operations. If you perform some operation, e.g. z = x + y, that works exactly over abstract, real numbers, but the result of x + y may not at all be exactly representable in floating-point. Thus, the result will be rounded, and a test that e.g. does assert_eq!(z, x + y) will fail.

There is a huge difference between arithmetic operators massaging results into a format that's still representable, and comparisons being non-deterministic. Floating-point comparisons are completely deterministic and well-behaved. assert_eq!(1.0, 1.0) won't ever fail. However, an operation that is expected to yield exactly 1.0 when performed over the reals won't necessarily yield exactly 1.0 when performed on floats.

1 Like

I'll just point out this Clippy suggestion is bad. (y - 1.23f64).abs() < f64::EPSILON is actually exactly equivalent to y == 1.23f64.

< f64::EPSILON is just too small to allow any margin of error here.

4 Likes

I don’t think this is true. EPSILON has to be large enough to work with the numbers around f64::MAX, with their coarse grained steps. The fine grained numbers around zero are much smaller than EPSILON.

No. The machine epsilon is the difference between 1 and the next representable number, so x == x + delta is always true if delta < epsilon and 1 <= x < 2.

You are thinking about subnormals, but since subnormals are smaller than the epsilon, adding a subnormal to 1 is still going to be exactly 1. Playground.

3 Likes

Documented in f32::EPSILON.

pub const EPSILON: f32 = 1.1920929E-7f32;

This is the difference between 1.0 and the next larger representable number.

More general: Machine epsilon - Wikipedia

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.