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

In f32 - Rust, the document just specifies the result of the comparison between two special numbers, however, I'm more interested in How the result will be when comparing two normal numbers. The document just refers to the IEEE 754 standard, however, I don't find any useful information in that document. I'm concerned about the case where two numbers are equivalent on Mathematical but it will be compared inequivalent in the program. So, I wonder how Rust processes the comparison between two normal numbers.


fn main() {
  let a = 19.62f32;
  let b = 11.62 + 8.00;
  println!("{}", a==b);  // false
}

The problem happens before the comparison. f32 can only approximate 19.62 and 11.62. You can't expect the addition to reproduce the exact same result.

Even though a and b are only approximations of decimal numbers, they are exact binary numbers. The comparison compares those as expected.

There's a reason clippy has a lint for this [1]. No matter which real-number-approximation you choose, comparisons involving the machine epsilon are going to appear in the recommendations.


  1. And the linked The Floating-Point Guide - Comparison is a good resource, too. ↩ī¸Ž

2 Likes

I come to the conclusion half the problem people have with floating point numbers and computers is that our languages let us write incorrect literal values in the fist place. That is to say we can write literal values that cannot be represented in the binary floating point format. Example:

We write 0.3
We get in our variable 00111110100110011001100110011010
Which is actually the number 0.300000011920928955

Clearly our language and compiler should not let us make this error. Especially if it prioritises correctness as Rust does.

Contrast to the situation with the integers. We would not expect to get away with writing:

let x: i32 = 0.5

The compiler will complain that `0.5´ is not an integer.

Clearly the compiler should complain that 0.3 is not a floating point value. It cannot exist in the IEEE 754 standard or our computer.

Suggest an error message like:

     let x: i32 = 0.3;
   |            ---   ^^^ expected `f32`, found real number
   |            |
   |            expected due to this
help: consider using 0.300000011920928955

With this correction to the compiler half the complaints about floating point operations would disappear as people would immediately know what they are dealing with.

:slight_smile:

9 Likes

The linked article is a good suggestion for comparing two f32 numbers. However, I wonder how two numbers are compared in a traditional way(i.e. x == y or x != y).

So, what is performed when comparing two f32 numbers? Is there any information about this?

Aside from NANs, they are simply compared as bit sequences. The problem is that these bit sequences can be slightly different from what you expect if you look at the calculations as if they were performed with real numbers.

Thanks. This is what I am asking here. Do (negative/positive)infinity numbers compare bit sequences too? Is this part documented in the IEEE 754 standard?

There are exceptions, such as comparisons between zero and negative zero. They have different binary representations but evaluate to the same value. This is mostly inconsequential in practice but it's good to know for trivia night.

I asked the question for the normalized numbers. Are comparisons of them just bit sequences comparison and where provenance in the IEEE 754 is this interpretation from?

I don't understand the question because you can't ignore subnormal numbers, signed zero, infinities, and NaN when you are dealing with f32.

I just talk about the subset of the representable numbers, that is the numbers other than NAN, INFINITY, and ZERO.

Assuming that Rust implements IEEE-754 semantics (which is not true on some platforms), two normal f32 numbers compare equal if they correspond to the same real number, and unequal if they don't.

In your example:
19.62f32 = 10011.1001111010111000011 in binary
11.62f32 = 1011.10011110101110000101
11.62f32 + 8f32 = 10011.1001111010111000010 (the last bit of 11.62f32 got rounded off)

So 19.62f32 and 11.62f32 + 8f32 differ in the last bit, and hence they correspond to different real numbers and compare unequal.

1 Like

According to your interpretation by using the binary sequences, did you mean they compare equally if their bit sequences are the same? Instead of saying they represent the same "real number"

For normal numbers that is the same thing. For zero that is not the same thing because two f32 values with different representations (+0.0 and -0.0) correspond to the same real number 0 and hence compare equal.

1 Like

The actual standard is unfortunately not available freely - the IEEE charges around USD 100 for non-members to access the standard, and they are fairly vigilant about keeping unauthorized copies off the Web. That makes providing specific citations a bit challenging.

However, should you find yourself in possession of a copy - perhaps from your local library, or if you're at a university that has an IEEE membership, then through institutional access - then page 43 (section 5.11) describes equality in general. Quote: "Infinite operands of the same sign shall compare equal." The IEEE 794-2019 specification does not elaborate further; how those comparisons are performed is up to the implementation.

Rust generally delegates that to the underlying hardware, where possible, but even where it must be emulated, you can assume that two equations producing infinities will produce equal infinities if they have the same sign, and unequal infinities if the signs differ.

Your answer is about Infinity, but how about the numbers that are not NAN, ZERO, and INFINITY? How do they compare? It seems that most answers say that they are compared by using bit sequences.

I don't understand what you are saying. Equality is fully specified by the standard, there is no leeway for the implementation or the hardware.

1 Like

IEEE 794-2019 does not explicitly define how comparison should work, only that it should have specific properties. From s. 5.11:

Four mutually exclusive relations are possible: less than, equal, greater than, and unordered; unordered arises when at least one operand is a NaN. Every NaN shall compare unordered with everything, including itself. Comparisons shall ignore the sign of zero (so +0 = -0). Infinite operands of the same sign shall compare equal.

Language standards shall define the comparison predicates in Tables 5.1 and 5.2. These predicates deliver a true-false response and are defined in pairs. Each predicate is true if and only if any of its indicated relations is true. Applying a prefix such as NOT to negate a predicate reverses the true-false sense of its associated entries, but does not change whether unordered relations signal an invalid operation exception.

The referenced tables go on to define signalling and non-signalling operations in terms of equal, greater than, less than, or unordered, in various combinations, as well as constraining that certain pairs of operations must be negations of one another. I'm not going to transcribe them here, as they're fairly obvious.

As far as formats go, the section is surprisingly terse:

For every supported arithmetic format, it shall be possible to compare one floating-point datum to another in that format (see 5.6.1). Additionally, it shall be possible to compare one floating-point datum to another in a different format as long as the operands' formats have the same radix.

Section 5.6.1 is merely a list of the comparison operations. Per the list in Annex C, the compare* family of operations are not specified anywhere other than in these two sections. Rust is free to implement them as the language designers see fit, subject to the constraints above (and to good sense).

Can you give an example? For what pair of values you think the standard doesn't specify whether equality should return true or false?