Various equalities of NaN?

I know of three functions for checking for equality: ==, ===, and isequal. The documentation comments on subtlety regarding floating point numbers, and there is indeed a difference:

julia> NaN == NaN
false

julia> isequal(NaN, NaN)
true

julia> NaN === NaN
true

Could you explain why they behave this way? I am particularly interested in

  1. Why isequal was designed to behave differently from == for float numbers.
  2. Why NaN === NaN is true. For most other types, I would have naively assumed a === b implies a==b since x===y
 Determine whether x and y are identical, in the sense that no program could distinguish them.

according to the documentation.

In terms of my knowledge level, I know floating point numbers are represented as bitstrings with a sign bit, exponent bits, and mantissa bits, but don’t really know much else.

Thanks in advance!

How NaN are handled is outlined in IEEE-754. All comparisons between NaN and any number including itself return false.

=== on the other hand is a different operation that checks that no operation could tell them apart. For mutable structs this is pointer equality and for immutable structs this is bitwise equality.

There are actually several different representations for NaN’s, which are not ===. The best way to check if a value is NaN is with the function isnan

10 Likes

Thanks for pointing me to IEEE-754. I’ve always thought of == as checking for equality of value, and did not know there was a specification given by IEEE. Now it makes sense that NaN==NaN is false but Nan === Nan.

Do you also happen to know why isequal was designed to return true for two NaN in Julia?

NaNs not being equal to each other can often be unhelpful behavior. For those cases isequal is simpler then having to explicitly handle NaN’s.

=== isn’t much help for those cases as there can be multiple different bitwise representations of NaNs.

1 Like

As @WschW notes, there is only one best practice when it comes to determining if one or more values is “not a number” (NaN): use isnan(x) or, to determine if two values are NaNs, use e.g. nans(x,y) = isnan(x) && isnan(y) . And, fortunately, isnan(x) runs fast.

2 Likes

isequal is used in hashing, so it has to work like this. See

for a related discussion.

2 Likes

For floats isequal is defined differently then ===. you can see the definition for how the former handles floats is:

isequal(x::AbstractFloat, y::AbstractFloat) = (isnan(x) & isnan(y)) | signequal(x, y) & (x == y)

This function returns true if both values are NaN’s.

This is distinct behaviour from === which in this case would return false if either the sign or any part of the mantissa differed.

1 Like

Ah that makes sense. Did not know isequal was the function used for hashing. Thanks!

Thank you all for clarifying the differences as well as why they exist!

Additional info: sometimes NaN === NaN seems not satisfied.

julia> NaN, Inf-Inf, 0/0
(NaN, NaN, NaN)

julia> NaN === Inf-Inf
false

julia> NaN === 0/0
false

julia> Inf-Inf === 0/0
true

julia> versioninfo()
Julia Version 1.8.4
Commit 00177ebc4fc (2022-12-23 21:32 UTC)
Platform Info:
  OS: Linux (x86_64-linux-gnu)
  CPU: 16 Ă— AMD Ryzen 7 2700X Eight-Core Processor
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-13.0.1 (ORCJIT, znver1)
  Threads: 1 on 16 virtual cores

This is because NaN may have different internal expressions.

julia> bitstring(NaN)
"0111111111111000000000000000000000000000000000000000000000000000"

julia> bitstring(Inf-Inf)
"1111111111111000000000000000000000000000000000000000000000000000"

See IEEE 754 - Wikipedia for more information.

The representation of NaNs specified by the standard has some unspecified bits that could be used to encode the type or source of error; but there is no standard for that encoding.

I think part of the confusion arises from the show method printing -NaN as NaN:

julia> bitstring(NaN)
"0111111111111000000000000000000000000000000000000000000000000000"

julia> bitstring(-NaN)
"1111111111111000000000000000000000000000000000000000000000000000"

julia> -NaN
NaN

julia> NaN ≡ -NaN
false

julia>  NaN ≡ -(Inf-Inf) ≡ -(Inf*0)
true

julia> -NaN ≡   Inf-Inf  ≡   Inf*0
true
3 Likes

It’s not just about the sign of NaN.

A IEEE754 double precision (Float64) value has 1 sign bit, 11 exponent bits, and 52 significand bits. There are a few major categories of numbers:

  • If the sign bit is 1 the value is negative-signed, if 0 then positive-signed
  • If the exponent bits are all 1 AND the signficand bits are all 0, the value is ±Inf
  • If the exponent bits are all 1 AND any significand bit is 1, the value is NaN
  • If the exponent bits AND significand bits are all 0, the number is ±0.0
  • If the exponent bits are 0 AND any signficand bit is 1, the value is “subnormal”
  • Else (if the exponent is not all 0 or all 1) the number is in the normal range

See wikipedia for more info.

So it’s not just that we don’t print the sign of NaN. There is the 52 bits of payload that we also don’t print. The reason we don’t print them is that they really don’t matter (for mathematical purposes). Arithmetic operation on a NaN will return the “same” result (modulo the sign and payload of a NaN, assuming the result is NaN) regardless of what NaN is passed in. There are some non-arithmetic functions (such as signbit(NaN), copysign(1.0,NaN)) where the sign of the NaN may affect the result.

There are 2^53-2 distinct representations of 64-bit NaN (2 different signs, 2^52-1 different significands since one is used by Inf). Each will only be === with exactly itself (=== requires the bit patterns to be identical). Note, also due to IEEE754 semantics, that -0.0 == 0.0 (but still -0.0 !== 0.0). As a bit of trivia, == and === are equivalent for IEEE floats so long as at least one argument is neither -0.0, +0.0, or a NaN.

Note that I don’t think Julia (or your hardware, which Julia defers to in most cases) usually produces 64-bit NaNs other than 0x7ff8000000000000 and 0xfff8000000000000 by “natural” means. But you certainly can introduce them via things like reinterpret(Float64,-1).

IEEE754 has nothing to say about ===. It’s a different construct for an entirely different purpose than anything IEEE754 cares about. Without ===, NaNs are pretty much interchangeable modulo the effect of the sign bit on some operations.

4 Likes

You also have (so rather use isequal and !isequal):

julia> NaN32 !== NaN  # There's also NaN16 for Float16, and there are different such e.g. bfloat, and conceivably NaN8, Float8 is available in a package I believe, also Posits have NaR.
true

while:

julia> NaN !== NaN
false

x === y can only be true if typeof(x) == typeof(y). So NaN32 !== NaN64 just like 1.0f0 !== 1.0e0.

1 Like