NaN is not equal to NaN!
Yeah, a Not-A-Number is Not equal to itself. But unlike the case with undefined and null where comparing an undefined value to null is true but a hard check(===) of the same will give you a false value, NaN’s behavior is because of IEEE spec that all systems need to adhere to. In this blog I will throw some light on NaNs and how they are implemented in the v8 engine.
Short Story: According to IEEE 754 specifications any operation performed on NaN values should yield a false value or should raise an error.
What is a NaN?
As per IEEE standards any value with all bits of the exponent set to 1 and at least one bit of the fraction/mantissa set as non-zero, represents a NaN.
To understand the above, we need to look at the basics of how numbers are saved in memory. IEEE 754 outlines the basic standards and computational algos to work with numbers or floating point numbers, explaining the complete 754 will take a separate blog altogether which I will write later on, for now lets just get an overview of it to understand NaN’s definition.
In a scientific representation of any number, a number has a sign(+/-), a mantissa and an exponent. For e.g.
A base 10 number 1.234 in scientific representation can be written as 1234 * 10^-3, where 1234 is a mantissa/significand and 10^-3 is the exponent.
IEEE suggests the use of the similar representation to save numbers in binary format, for a single precision float value the IEEE format is given as
| sign bit | exponent | mantissa | where,
sign bit = 0 | 1
exponent = -127 to +128
mantissa = 24 bits of significand value
Numbers can be written in 2 formats in accordance to the above representation
Single: SEEEEEEE EMMMMMMM MMMMMMMM MMMMMMMM
Double: SEEEEEEE EEEEMMMM MMMMMMMM MMMMMMMM MMMMMMMM MMMMMMMM MMMMMMMM MMMMMMMM
So a NaN value according to the definition(all exponent set to 1 and at least one fraction bit set) can be represented as:
| 0 | 1111111 | 00...01
or
| 0 | 1111111 | 10...00
Tip: Exponent value -127 and +128 are used for reserved category, NaN is +128.
Types of NaN
There are 2 types of NaN, one can speak another one cannot.
Quiet NaN
A Quiet NaN is simply any NaN value that doesn’t raise error and can flow through your code by falsifying value or converting itself to a string.
Signaling NaN
This one can talk, NaN values in this case will raise or set error for the user to handle it, most of the strongly typed languages make use of signaling NaNs.
Implementation of NaNs
NaN’s implementation and differentiation between quiet and signaling NaN is often left on the underlying system, std::numeric_limits helper is generally used in C++ to yield a NaN value, if you drill down in the header files of std you will find out the default implementation hidden deep inside the file math.h which in turn make use of builtin functions of limits.h which looks like this
// Quiet NaN// float NaN
static const unsigned int _QNAN_F = 0x7fc00000;
// long double NaN
static const unsigned int _QNAN_LDBL128[4] = {0x7ff80000, 0x0, 0x0, 0x0};// Signaling NaN// float NaN
static const unsigned int _SNAN_F= 0x7f855555;
// double NaN
static const unsigned int _SNAN_D[2] = {0x7ff55555, 0x55555555};
// long double NaN
static const unsigned int _SNAN_LDBL128[4] = {0x7ff55555, 0x55555555, 0x0, 0x0};
The differentiation between multiple types of NaN is done on the basis of their masked bits. If you see the above code closely you will find out that all the values adhere to the initial definition of NaN as defined by IEEE 754.
NaNs in v8
In v8 compiler all data types are characterized and saved with a proper bit assigned to it so as to boost the runtime comparison and operations, NaNs are no exception.
v8 also make use of std::numeric_limits helpers of C++ to define NaN values, however v8 too have its own custom NaN helpers which works in a similar fashion. In v8 a NaN value can be encountered at 2 levels, one is at compile time and another at runtime, lets have a look at both.
Compile Time NaNs
Whenever the v8 compiler encounters a NaN constant value during compilation/graph creation it automatically assigns the value of a quiet NaN to it.
Runtime NaNs
During runtime a quiet NaN value could be returned by parseInt and parseFloat or they could be returned by any math operation or a MIPS check.
All variables in v8 are typed, there are 2 Typer classes which does the task of typing of variables, one belongs to TurboFan and another one is the ASMTyper, to understand typing and v8 compiler flow you may enjoy Benedikt’s slides.
Its these typers which are responsible for type checking your variables before evaluating them, so for a comparator call on a NaN value you will end up triggering this in TurboFan
If either of the RHS or LHS value is NaN, comparator will always return false, which is right keeping in mind the IEEE spec. By default NaNs in JS are quiet NaNs so if future TurboFan compiler wants to add signaling NaNs functionality then they will just check for the masked bits and then trigger the error.
Oh are we done? Damnit!
Yeah folks that’s it, a dive inside the little world of our NaNs, they now feel so special.
And wait, you might sometime end up with NaN values becoming 0.0000 if you compile your C++ code with -ffast-math flag on Intel processors.
(╯°□°)╯︵ ┻━┻