In Node.js, when we serialize a floating point number, we deserialize it:

Var f = 2.3; var buffer = new Buffer(1024); buffer.writeFloatBE(f); var g = buffer.readFloatBE(); console.log(f == g);Copy the code


We find that we can no longer get the previous value:





However, if serialized back, the result is the same:


Why is that?

Identify the areas where problems occur


Is the problem with serialization? To do this, we wrote a C program to serialize the same floating point number:


Note that 51, 51, 19, 64 are equivalent to 0x33, 0x33, 0x13, 0x40. After running, we find that the serialization results are exactly the same except for the difference between the small encoder and the big encoder:

As you can see, the serialization of C and Node.js is exactly the same, without any problems.


Next, let’s see what happens when we deserialize this floating point number in C:


C language to decimal binary floating point number can get the correct result, we determine that the problem is likely to be node.js binary floating point number to decimal algorithm.

How does C convert a binary floating point number to decimal


We found the glibc source code and found the logic in the ___printf_fp function of printf_fp.c. The function has over a thousand lines, so converting a binary floating point number to decimal is not an easy task:

A brief reading of the source code shows that it takes the system’s current locale (considering the LC_ALL environment variable) and first finds the correct decimal representation (for example, a comma in German, a period in English). Then take out each part of floating point number, call various MPN high-precision library functions to carry out various high-precision operations, and finally calculate the decimal representation of floating point number.

For such a famous standard library, a function writing 1000 lines, the author must have implemented a famous algorithm, otherwise how to maintain such a complex logic? Sure enough, we found the algorithm used, Dragon4, in a CHANGELOG record from 1992.

How does the JS language convert a binary floating point number to decimal


So what are JavaScript algorithms? We find an explanation in ECMA section 9.8.1. It turns out that JS has followed a complex set of algorithms for presenting numbers for years. The end of the standard section also prompts JS implementers to refer to the paper Correctly Rounded binary-Decimal and Decimal-Binary Conversions by David M. Gay (1990).


ECMAScript Language Specification – ECMA-262 Edition 5.1

Source code analysis


First, a reading of v8’s source code reveals that it does not use David M. Gay’s naive algorithm. In 2010 Florian Loitsch made the biggest advance on the subject in 20 years with his paper “Printing Floating – Point Numbers Quickly and Sloppily with Integers,” A new algorithm, Grisu3, was proposed, which allowed V8 to compute the decimal representation of binary floating-point numbers without a high-precision library, almost beating dragon4. The only downside is that 0.5% of floating point conversions will fail, and v8 can intelligently fallback to high-precision calculations to get them.


Second, output 2.3 is not a mathematically correct result. Some mathematical analysis reveals that some binary floating point numbers will never be converted to beautiful finite decimal numbers. In the decimal system, a rational number is an infinite decimal as long as its denominator contains factors other than the prime factor of 10. For example, 1/15 is equal to 0.066666 because 15 has a factor of 3. Fermat’s little theorem can be used for this conclusion, taking a=10, p=2 or 5:

Similarly, in the binary system, a rational number is an infinite decimal as long as its denominator contains factors other than the prime factor of 2. Thus, the denominator of the rational number 2.3 = 23/10 in this example has prime factors of 2 and 5, including factors other than 2, and thus the representation under binary is infinite.

Computers are finite, however, so accuracy is lost the moment floating-point is serialized to four bytes. When deserialization comes back, the program that outputs 2.3 is mathematically incorrect, and the program that outputs 2.299999952316284 is mathematically correct.


The reality is harsh.

How did 2.3 come about


By default, printf is rounded by a number of significant digits. If we increment the number of significant digits:


It can be found that when the number of significant digits after the decimal is set to 1 ~ 7, it is exactly rounded to the decimal result we want, otherwise there is no egg:

Node.js has no float at all

But come to think of it, even though floating point numbers are brutal in the real world, Node.js does something wrong. Why can two numbers with the same binary representation be given two results by toString()? And node.js also rejects the idea that they are equal?

The reason is node.js has no hair at all! There is no float! There are only two internal implementations of the JS Number object in V8: smI (small integer) and double. Grisu3’s implementation is also encapsulated only in functions such as DoubleToCString, and there is never a FloatToCString function that handles float.


To make matters worse, float is silently cast to double as soon as readFloatBE gets a float value from V8. B: That is, when we enter, we promote double; when we leave, we pawnchess.


Therefore, their binaries are actually inconsistent because we only looked at the first four bytes, not the last four:

The solution


First, if really want to get 2.3 like C, you need to artificial specified integer figures, with a Number. The prototype. The toPrecision function after rounding to instantiate the Number object:

Second, avoid using float as much as possible. Double is now widely supported by servers and there is no need to use 32-bit instead of 64-bit to save memory or bandwidth. Especially in the world of Node.js without float, the use of float as a data storage or data exchange format inevitably introduces all sorts of counterintuitive problems.


Finally, in high-precision situations, performance should be sacrificed by abandoning machine-native floating point numbers in favor of infinite precision libraries such as BigDecimal.