Last time I encountered a strange problem: JS (2.55).tofixed (1) output is 2.5, instead of 2.6 rounded, why?

Further observation:



It turns out that not all of them are abnormal. The rounding of 1.55 is still correct. Why not 2.55 and 3.45?

This requires us to find the answer in the source code.

In V8, there are two types of number storage: Smi for small integers and HeapNumber for all numbers except small integers. Smi is placed directly on the stack, while HeapNumber requires new memory and is placed in the heap. We can simply draw where the heap and stack are in memory:



The following code:

let obj = {};Copy the code

So here we define a variable obj, obj is a pointer, it’s a local variable, it’s on the stack. The curly braces {} instantiate an Object that needs to occupy the memory requested in the heap, and obj refers to the location of the memory.

Compared to heap, stack to read than a pile of high efficiency, because the variables can be through the memory bias on the stack location, such as function lose a variable takes the space entry address (to lower address growth), you can get the variables in the built-in memory, addressing, and heap need through Pointers so heap is slower than the stack (although stack available space than a pile of small lot). Therefore, local variables such as Pointers and numbers, which take up less space, are usually stored on the stack.

For the following code:

let smi = 1;Copy the code

Smi is a Number of type Number. If this simple number had to be placed in the heap and a pointer to it, it wouldn’t be worth it, either in terms of storage space or read efficiency. So V8 has a class called Smi, which is not instantiated, and whose pointer address is the value of the number it stores, not the heap space. Since a pointer is itself an integer, it can be used as an integer, which in turn can be typed to an Smi instance pointer to call Smi functions, such as getting the actual integer value.

The following source code comments:

// Smi represents integer Numbers that can be stored in 31 bits.
// Smis are immediate which means they are NOT allocated in the heap.
// The this pointer has the following format: [31 bit signed int] 0
// For long smis it has the following format:
//     [32 bit signed int] [31 bits zero padding] 0
// Smi stands for small integer.Copy the code

On normal systems int is 32 bits, the first 31 bits are used to represent the value of an integer (plus or minus), and the first 32 bits are used to represent the value of an integer if it is 64 bits. So there are 31 bits to represent the data in 32 bits, subtract one sign bit, and that leaves 30 bits, so the maximum integer for Smi is:

2 ^ 30-1 = 1073741823 = 1 billion

About a billion.

At this point you might ask yourself, why bother with a Smi class instead of just using a base type like int? This is probably because the representation of JS data in V8 inherits from the root class Object (note that Object is not JSObject; JSObject corresponds to V8 JSObject), allowing for some general processing. So small integers need a class, but they can’t be instantiated, so we use Pointers to store values.

More than 2.1 billion and decimals are stored using HeapNumber. Like JSObject, the data is stored in the heap. HeapNumber stores a double precision floating point number, 8 bytes = 2 words = 64 bits. On the storage structure of double-precision floating-point numbers, I have written why 0.1 + 0.2 Does not Equal 0.3? They gave a very detailed introduction. Here can be briefly mentioned, such as the source code definition:

  static const int kMantissaBits = 52;
  static const int kExponentBits = 11;Copy the code

Of the 64 bits, the mantissa takes up 52 bits, while the exponent uses 11 bits and one sign bit. When the double space is used to represent integers, it is the space of the 52-mantissa. Since integers can be represented exactly in binary, the maximum value of the 52-mantissa plus the hidden integer 1 (see previous article) can be 2 ^ 53-1:

// ES6 section 20.1.2.6 number. MAX_SAFE_INTEGER const double kMaxSafeInteger = 9007199254740991.0; / / 2 ^ 53-1Copy the code

This is a 16-bit integer, so you know that the exact number of digits for a double floating-point number is 15, and that you can assume the 16th bit is correct 90 percent of the time.


So we know how numbers are stored in V8. For 2.55, which is a double-precision floating-point number, printing the 64-bit memory of 2.55 looks like this:

ToFixed (1) for (2.55).tofixed (1), the source is so carried out, the first integer 2 out, converted into a string, and then the decimal out, according to the number of digits specified by the parameter is rounded, and then a decimal point in the middle, you get the rounded string result.

How do I take the integer part? The mantissa of 2.55 (plus the hidden 1) is a:

1.01000110011…

Its exponent bit is 1, so if you move this one bit to the left you get b:

10.1000110011…

If you move A to the left by 1, it becomes a 53-bit number. If you move B to the right by 52-1, you get a binary 10, which is a decimal 2. Subtract the value of 10 by 51 from b, and you get the decimal part. The actual calculation works like this:

Uint64_t integrals = significand >> -exponent; Uint64_t fractionals = (integrals << -exponent);Copy the code

Next question — how do integers turn into strings? The source code is as follows:

static void FillDigits32(uint32_t number, Vector<char> buffer, int* length) {
  int number_length = 0;
  // We fill the digits in reverse order and exchange them afterwards.
  while(number ! = 0) { char digit = number % 10; number /= 10; buffer[(*length) + number_length] ='0' + digit;
    number_length++;
  }
  // Exchange the digits.
  int i = *length;
  int j = *length + number_length - 1;
  while (i < j) {
    char tmp = buffer[i];
    buffer[i] = buffer[j];
    buffer[j] = tmp;
    i++;
    j--;
  }
  *length += number_length;
}Copy the code

The digit is the ASCII code of the digit plus the ASCII code of the digit 0, which is a char. In C/C++/Java/Mysql, char is a variable represented by a single quote. The ASCII symbol is represented by a byte. The actual value stored is its ASCII encoding, so it can be converted to and from integers, such as ‘0’ + 1 to get ‘1’. For each unit digit, divide by 10, which is the decimal equivalent of moving right one digit, and then continue to process the next unit digit, continually adding it to the char array (note that in C++ we divide integers by decimals, not JS).

And finally, I’m going to reverse this array, because I’m going to get the units digit out front.


How do I rotate the decimal part? The following code looks like this:

int point = -exponent; // Exponent = -51 // fractional_count indicates the number of decimal places to keep, and toFixed(1) equals 1for (int i = 0; i < fractional_count; ++i) {
  if (fractionals == 0)
    break;
  fractionals *= 5; // fractionals = fractionals * 10 / 2;
  point--;
  char digit = static_cast<char>(fractionals >> point);
  buffer[*length] = '0' + digit;
  (*length)++;
  fractionals -= static_cast<uint64_t>(digit) << point;
}
// If the first bit after the point is set we have to round up.
if (((fractionals >> (point - 1)) & 1) == 1) {
  RoundUp(buffer, length, decimal_point);
}Copy the code

ToFixed (n) converts the first n decimal digits into a string, and then sees if the value of n + 1 is carried by one digit.

When converting the first n decimal places to a string, we multiply the decimal place by 10, and then move it to the right by 50 + 1 = 51 to get the first decimal place (the code is multiplied by 5, mainly to avoid overflow). When you multiply the decimal place by 10, the first decimal place goes to the whole number, and then you move it to the right and you lose the last 51 places, because the last 51 must be a decimal place, so you get the first decimal place. And then you subtract the whole number and you get the decimal number that’s left after you subtract 1 decimal place, because you’re only going around once so you’re out of the loop.

It then checks if it needs to be rounded, and it checks if the first bit of the remaining mantissa is 1, if it is 1, otherwise it doesn’t process. After subtracting the first decimal place from above, 0.05 remains:

The actual stored value is not 0.05, but a little less than 0.05:

Since 2.55 is not exactly represented and 2.5 is, 2.55-2.5 yields the stored value of 0.05. You can see it’s actually less than 0.05.

According to the source code, if the remaining mantissa bit is not 1, it is not carried, because the remaining mantissa bit is 0, so it is not carried, so it results in (2.55). ToFixed (1) input result is 2.5.

The root cause is that 2.55 is stored a bit smaller than it actually is, so the first mantissa of 0.05 is not 1, so it is dropped.


So what to do? Can’t we use toFixed?

Knowing why, we can make a correction:

if(! Number.prototype._toFixed) { Number.prototype._toFixed = Number.prototype.toFixed; } Number.prototype.toFixed =function(n) {
    return (this + 1e-14)._toFixed(n);
};Copy the code

ToFixed is to add a very small decimal, the decimal experiment, 1E-14 on the line. What might that do, might it cause a carry that shouldn’t be carried? Adding a 14-digit decimal might cause 13 digits to carry 1. But if these two numbers are 1e minus 14 away from each other, they’re almost equal, so the effect of adding this is almost negligible, unless you want to be very precise. This is just a fraction of the Number.EPSILON:

After this, toFixed is normal:


This article through V8 source code, explains how the number in memory is stored, and memory stack, heap storage to do a popular, discussed the source code toFixed is how to carry, cause no carry is what, how to make a correction.