1
头图

1 background

The seller advertises today, and the new exposure rate is 1000, and the total product exposure reaches 11,500. I am very happy. When I look at the platform, it shows an exposure rate of 1.1w. I am confused. If calculated by rounding, it should be 1.2w, right? What's going on?

Through investigation, it was found that it was a bug toFixed

2 What is toFixed?

Definition from MDN

toFixed() returns a string representation of numObj that does not use exponential notation and digits has exactly the number after the decimal point. If necessary, the number will be rounded to , and if necessary, the decimal part will be filled with zeros to make it have the specified length. If the absolute value of numObj is greater than or equal to 1e+21, this method calls Number.prototype.toString() and returns a string in exponential notation.

And there is a warning below:

Warning: Floating point numbers cannot accurately represent all decimals in binary. This may cause unexpected results, for example 0.1 + 0.2 === 0.3 returns false.

Test through the Chrome browser console:

(1.15).toFixed(1)
// "1.1"
(1.25).toFixed(1)
// "1.3"
(1.35).toFixed(1)
// "1.4"
(1.45).toFixed(1)
// "1.4"
(1.55).toFixed(1)
// "1.6"

It can be seen from the above that toFixed is indeed problematic in some examples. JS does not distinguish between integers and floating-point numbers. As long as the Number type is a floating-point number, it uses the 64-bit double-precision format IEEE 754

3 What is IEEE 754

Definition from Baidu Encyclopedia:

The IEEE Standard for Binary Floating-Point Arithmetic (IEEE 754) is the most widely used floating-point arithmetic standard since the 1980s and is adopted by many CPUs and floating-point arithmetic units. This standard defines the format of floating-point numbers (including negative zero-0) and denormal numbers), some special values (infinity (Inf) and non-numerical values (NaN)), and the "floating-point number operators" for these values "; It also specifies four numerical rounding rules and five exceptions (including the timing and handling of exceptions).
IEEE 754 specifies four ways to represent floating-point values: single precision (32 bits), double precision (64 bits), extended single precision (above 43 bits, rarely used) and extended double precision (79 bits) The above is usually implemented in 80 bits). Only the 32-bit mode is mandatory, the others are optional. Most programming languages provide IEEE floating-point number format and arithmetic, but some list them as unnecessary. For example, the C language that existed before IEEE 754 includes IEEE arithmetic, but it is not a mandatory requirement (the float in C language usually refers to IEEE single precision, and double refers to double precision).

The IEEE floating-point number standard logically uses triples {S, E, M} to represent a number V, that is, V=(-1)S×M×2^E:

Take the value of 9.625 IEEE 754 standard 64-bit double-precision as an example:

[S (sign bit)] [E (exponent bit)] [M (significant digit bit)]
[ 0 ] [ 10000000010 ] [ 0011010000000000000000000000000000000000000000000000]

position description
Sign bit s (Sign) Determine whether the number is positive (s=0) or negative (s=1), and the interpretation of the sign bit of the value 0 is treated as a special case;
Exponent E (Exponent) Is a power of 2 (may be a negative number), and its function is to weight floating-point numbers;
Significand M (Significand) It is a binary decimal, and its value range is 1~2-ε, or 0~1-ε. It is also called Mantissa, Coefficient, and even “decimal”;

Wherein the exponent value = the real exponent value + the offset value (1023), the offset = 2^(k-1)-1, where k represents the 11 digits of the exponent;

3.1 Exponential offset

Because the exponent can be positive or negative, in order to deal with the negative exponent, the actual exponent value needs to be added with an offset (Bias) value as the value stored in the exponent section as required.

3.2 Exponent bit

Standardization: S + (E!=0 && E!=2047) + 1.M
Denormalization: S + 000 00000000 + M
Infinity: S + 111 11111111 + 00000000 00000000 00000000 00000000 00000000 00000000 0000
Infinity variant (NAN): S + 111 11111111 + (M!=0)

Normalized situation: In the above general situation, because the order code cannot be 0 or 2047, the exponent cannot be -1023 or 1024. Only in this case will there be an implicit bit 1.

Non-standardization situation: At this time, the order code is all 0 and the exponent is -1023. If the mantissa is all 0, the floating-point number represents plus or minus 0; represents those very close to 0.0.

3.3 Effective mantissa bits

There are many ways to represent floating-point numbers. For example, 9.625 10^3 can be expressed as 0.9625 10^4, 96.25 * 10^2. In the IEEE floating-point number standard, according to scientific notation, the first digit can only be 1. For this reason, IEEE 754 omits this default 1, so the effective mantissa has 53 digits.

At this time, there is a problem. The 1 omitted from the mantissa M must exist, so that the floating point number cannot represent 0.0. How to represent it?
The sign bit is 0, the exponent section is all 0, and the decimal section is also all 0), which results in M=f=0. The strange thing is that when the sign bit is 1 and the other segments are all 0, the value -0.0 is obtained. According to the IEEE floating-point format, the values +0.0 and -0.0 are different in some respects.

3.4 Give an example

Take -9.625 to see the conversion process:

  1. The negative sign S is 1, and the absolute value is converted to binary: 1001.101, (the integer divided by 2 is the remainder, the decimal is multiplied by 2 is rounded, and arranged along the decimal point)
  2. Scientific notation: 1.001101 * 2 ^ 3
  3. Calculate the exponent bit: 00 000000011 (the true value of the exponent 3) + 011 11111111 (offset 1023) = 100 00000010
  4. The final stored value: 1[00110100 00000000 00000000 00000000 00000000 00000000 0000]

But not all decimal decimals can be represented by floating-point numbers. Take 1.15 as an example, and convert them to binary digits:
1.001001100110011001100110011001100110011001100110011……

Use 0011 to loop indefinitely, but for computers, the storage length is limited, so the final storage value is:
0[0010011001100110011001100110011001100110011001100110],

So 1.15 actually is 1.14999999999999991118215802999, is clearly seen, the rounding result is 1.1, which is because the IEEE 754 floating-point arithmetic standard cannot accurately represent decimal numbers in binary, resulting in the result of the rounding that does not meet the expected result.

4 JSCore toFixed source code implementation

4.1 ECMAScript specification

ecmaScript specification for Number.prototype.toFixed(fractionDigits) Implementation Specification :

toFixed returns a string containing the value of this Number, expressed in decimal fixed point notation, with fractionDigits after the decimal point. If fractionDigits is not defined, it is assumed to be 0.

Perform the following steps:

  1. Let x be thisNumberValue(this value).
  2. ReturnIfAbrupt ( x )。
  3. Let f be ToInteger (fractionDigits ). (If fractionDigits is undefined, this step will produce a value of 0).
  4. ReturnIfAbrupt ( f )。
  5. If f <0 or f> 20, a RangeError exception is thrown. However, it is allowed to implement the behavior of extending the toFixed value of f less than 0 or greater than 20. In this case, RangeError is not necessarily thrown for such values. toFixed
  6. If x is NaN, String "NaN" is returned.
  7. Let s be the empty string.
  8. If x <0, then

    1. Let the trumpet be "-".
    2. Let x = – x.
  9. If x ≥ 10 21, then

    1. Let m = ToString (x ).
  10. Otherwise x <10 21,

    1. Let n be an integer, and the exact mathematical value of n ÷ 10 f – x is as close to zero as possible. If there are two such n, choose the larger n.
    2. If n = 0, let m be String "0". Otherwise, let m be a string consisting of the digits of the decimal representation of n (in order, without leading zeros).
    3. If f ≠ 0, then

      1. Let k be the number of elements in m.
      2. If k ≤ f, then

        1. Let z be a string consisting of f +1 – k occurrences of code unit 0x0030.
        2. Let m be the concatenation of the strings z and m.
        3. Let k = f + 1.
      3. Let a be the first k – f elements of m, and let b be the remaining f elements of m.
      4. Let m be the concatenation of three strings, "." and b.
  11. Returns the concatenation of strings s and m.

According to the above statement, for (1.15).toFixed(1), we find two numbers:

11 / 10 - 1.15 //  -0.04999999999999982
12 / 10 - 1.15 // 0.050000000000000044

It can be seen that the result of the former is closer to 0, so 1.1 is taken.

If both are close to 0, take the larger integer of the two, for example, for 99.55:

995/10 - 99.55 // -0.04999999999999716
996/10 - 99.55 //  0.04999999999999716

At this time, it should be 99.6, but when we run (99.55).toFixed(1) in the browser console, we get 99.5. Isn't the browser implemented according to the specification?

4.2 Implementation of webkit javascript core toFixed

4.2.1 Webkit compilation and debugging

JSCrore/WebKit setting and debugging

4.2.1.1 Get webkit source code
# Clone the WebKit repository from GitHub
git clone git://git.webkit.org/WebKit.git WebKit.git
4.2.1.2 Build webkit

(1) xcode installation

# Install
$ xcode-select --install
already installed...

# Make sure xcode path is properly set
$ xcode-select -p
/Applications/Xcode.app/Contents/Developer

# Confirm installation
$ xcodebuild -version
Xcode 10.1
Build version 10B61

(2) Execute the script to build JSC (JavaScriptCore) as a debug build.

# Run the script which builds the WebKit
Tools/Scripts/build-webkit --jsc-only --debug

# jsc-only : JavaScriptCore only
# debug    : With debug symbols

Note: After installing cmake, path not found, add the cmake command to the environment variable, open the .bash_profile file in the home directory and add the following two sentences, save and modify:

# Add Cmake Root to Path
export CMAKE_ROOT=/Applications/CMake.app/Contents/bin/
export PATH=$CMAKE_ROOT:$PATH

(3) Set up lldb (lldb is a debugger similar to gdb. We can use lldb to debug jsc)

# Incase of a python error, run the following
$ alias lldb='PATH="/usr/bin:$PATH" lldb'

# Load the file to the  debugger
$ lldb ./WebKitBuild/Debug/bin/jsc
(lldb) target create "./WebKitBuild/Debug/bin/jsc"
Current executable set to './WebKitBuild/Debug/bin/jsc' (x86_64).
(lldb) run
Process 4233 launched: './WebKitBuild/Debug/bin/jsc' (x86_64)
>>> 

lldb related commands

x/8gx address #查看内存地址 address

next(n) #单步执行
step(s) #进入函数
continue(c) #将程序运行到结束或者断点处(进入下一断点)
finish #将程序运行到当前函数返回(从函数跳出)
breakpoint(b) 行号/函数名 <条件语句> #设置断点
fr v #查看局部变量信息
print(p) x #输出变量 x 的值

4.2.2 Source code analysis

4.2.2.1 Entrance, handling of various situations
EncodedJSValue JSC_HOST_CALL numberProtoFuncToFixed(JSGlobalObject* globalObject, CallFrame* callFrame)
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    // x 取值 99.549999999999997
    double x;
    if (!toThisNumber(vm, callFrame->thisValue(), x))
        return throwVMToThisNumberError(globalObject, scope, callFrame->thisValue());

    // decimalPlaces 取值 1
    int decimalPlaces = static_cast<int>(callFrame->argument(0).toInteger(globalObject));
    RETURN_IF_EXCEPTION(scope, { });

    // 特殊处理,略
    if (decimalPlaces < 0 || decimalPlaces > 100)
        return throwVMRangeError(globalObject, scope, "toFixed() argument must be between 0 and 100"_s);

    // x 的特殊处理,略
    if (!(fabs(x) < 1e+21))
        return JSValue::encode(jsString(vm, String::number(x)));

    // NaN or Infinity 的特殊处理
    ASSERT(std::isfinite(x));

    // 进入执行 number=99.549999999999997, decimalPlaces=1
    return JSValue::encode(jsString(vm, String::numberToStringFixedWidth(x, decimalPlaces)));
}

Continue to enter from the numberToStringFixedWidth method to the FastFixedDtoa processing method

It should be noted that the integer and decimal parts of the original value use exponential notation, which is convenient for subsequent bit operations.
99.549999999999997 = 7005208482886451 2 -46 = 99 + 38702809297715 2 -46

4.2.2.2 Separate integer part and decimal part
// FastFixedDtoa(v=99.549999999999997, fractional_count=1, buffer=(start_ = "", length_ = 122), length=0x00007ffeefbfd488, decimal_point=0x00007ffeefbfd494)

bool FastFixedDtoa(double v,
                   int fractional_count,
                   BufferReference<char> buffer,
                   int* length,
                   int* decimal_point) {
  const uint32_t kMaxUInt32 = 0xFFFFFFFF;
  // 将 v 表示成 尾数(significand) × 底数(2) ^ 指数(exponent) 
  // 7005208482886451 x 2 ^ -46
  uint64_t significand = Double(v).Significand();
  int exponent = Double(v).Exponent();

  // 省略部分代码

  if (exponent + kDoubleSignificandSize > 64) {
    // ...
  } else if (exponent >= 0) {
    // ...
  } else if (exponent > -kDoubleSignificandSize) {
    // exponent > -53 的情况, 切割数字

    // 整数部分: integrals = 7005208482886451 >> 46 = 99 
    uint64_t integrals = significand >> -exponent;
    // 小数部分(指数表达法的尾数部分): fractionals = 7005208482886451 - 99 << 46  = 38702809297715
    // 指数不变 -46
    // 38702809297715 * (2 ** -46) = 0.5499999999999972
    uint64_t fractionals = significand - (integrals << -exponent);
    if (integrals > kMaxUInt32) {
      FillDigits64(integrals, buffer, length);
    } else {
      // buffer 中放入 "99"
      FillDigits32(static_cast<uint32_t>(integrals), buffer, length);
    }
    *decimal_point = *length;
    // 填充小数部分,buffer 为 "995"
    FillFractionals(fractionals, exponent, fractional_count,
                    buffer, length, decimal_point);
  } else if (exponent < -128) {
    // ...
  } else {
    // ...
  }
  TrimZeros(buffer, length, decimal_point);
  buffer[*length] = '\0';
  if ((*length) == 0) {
    // The string is empty and the decimal_point thus has no importance. Mimick
    // Gay's dtoa and and set it to -fractional_count.
    *decimal_point = -fractional_count;
  }
  return true;
}
4.2.2.3 Truncate and carry the decimal part

FillFractionals is used to fill the decimal part, take a few digits, and whether to carry is handled in this method

// FillFractionals(fractionals=38702809297715, exponent=-46, fractional_count=1, buffer=(start_ = "99", length_ = 122), length=0x00007ffeefbfd488, decimal_point=0x00007ffeefbfd494)

/*
小数部分的二进制表示法: fractionals * 2 ^ exponent
38702809297715 * (2 ** -46) = 0.5499999999999972

前提:
  -128 <= exponent <=0。
  0 <= fractionals * 2 ^ exponent < 1 
  buffer 可以保存结果
此函数将舍入结果。在舍入过程中,此函数未生成的数字可能会更新,且小数点变量可能会更新。如果此函数生成数字 99,并且缓冲区已经包含 “199”(因此产生的缓冲区为“19999”),则向上舍入会将缓冲区的内容更改为 “20000”。
*/
static void FillFractionals(uint64_t fractionals, int exponent,
                            int fractional_count, BufferReference<char> buffer,
                            int* length, int* decimal_point) {
  ASSERT(-128 <= exponent && exponent <= 0);
  if (-exponent <= 64) { 
    ASSERT(fractionals >> 56 == 0);
    int point = -exponent; // 46

    // 每次迭代,将小数乘以10,去除整数部分放入 buffer

    for (int i = 0; i < fractional_count; ++i) { // 0->1
      if (fractionals == 0) break;

      // fractionals 乘以 5 而不是乘以 10 ,并调整 point 的位置,这样, fractionals 变量将不会溢出。然后整体相当于乘以 10
      // 不会溢出的验证过程:
      // 循环初始: fractionals < 2 ^ point , point <= 64 且 fractionals < 2 ^ 56
      // 每次迭代后, point-- 。
      // 注意 5 ^ 3 = 125 < 128 = 2 ^ 7。
      // 因此,此循环的三个迭代不会溢出 fractionals (即使在循环体末尾没有减法)。
      // 与此同时 point 将满足 point <= 61,因此 fractionals < 2 ^ point ,并且 fractionals 再乘以 5 将不会溢出(<int64)。


      // 该操作不会溢出,证明见上方
      fractionals *= 5; // 193514046488575
      point--; // 45
      int digit = static_cast<int>(fractionals >> point); // 193514046488575 * 2 ** -45 = 5
      ASSERT(digit <= 9);
      buffer[*length] = static_cast<char>('0' + digit); // '995'
      (*length)++;
      // 去掉整数位
      fractionals -= static_cast<uint64_t>(digit) << point; // 193514046488575 - 5 * 2 ** 45 = 17592186044415 
      // 17592186044415 * 2 ** -45 = 0.4999999999999716 
    }
    // 看小数的下一位是否值得让 buffer 中元素进位
    // 通过乘2看是否能 >=1 来判断
    ASSERT(fractionals == 0 || point - 1 >= 0);
    // 本例中 17592186044415 >> 44 = 17592186044415 * 2 ** -44 = 0.9999999999999432 , & 1 = 0
    if ((fractionals != 0) && ((fractionals >> (point - 1)) & 1) == 1) {
      RoundUp(buffer, length, decimal_point);
    }
  } else {  // We need 128 bits.
    // ...
  }
}

In this way, 995 is obtained, which is n in the specification description, and a decimal point is inserted after it is the final result of 99.5.

4.2.3 Summary

The js engine does not look for an n, so that n / (10 ^ f) is as equal to x as possible, but divides x into integer and fractional parts, and uses exponential notation to calculate separately.

When dealing with decimals, move the decimal point to the right. When using exponential notation, there is a detail considering that the base number 10 may cause overflow, and then the base number 5 is used, and the exponent decreases by 1. After the f-bit calculation, the next bit is finally calculated to see if a carry is needed.

Of course, the final result does not meet our daily calculations. The core is still in the IEEE 754 notation. The value of 99.55 in the initial debugging stage is 99.549999999999997.

5 Expansion

5.1 The maximum and minimum values that JS can represent

There are two concepts in the range of numbers, the largest positive number and the smallest negative number, the smallest positive number and the largest negative number.

From the three dimensions of S, E, and M, S means positive and negative, E means exponent means size, and M significant digits means precision.

Above we talked about normalization:
The maximum value of E is 111 11111110-011 11111111 (offset) = 011 11111111 = 1023, and the index value range is [-2^1023, 2^1023 ], that is, [-8.98846567431158e+307, 8.98846567431158e+307] ;
The maximum value of the significant digits of M is 11111111 11111111 11111111 11111111 11111111 11111111 1111, plus the default integer 1, the mantissa value is infinitely close to 2;
In summary, the largest positive number is infinitely close to 2 * (8.98846567431158e+307) = 1.797693134862316e+307, and the smallest positive number is infinitely close to -1.797693134862316e+307;
Let's look at the maximum value Number.MAX_VALUE = 1.7976931348623157e+308 defined by JS, which is very close to the maximum positive number we calculated;
So the range of numbers is [-1.7976931348623157e+308, 1.7976931348623157e+308]. If it exceeds this range, it will be displayed as Infinity or -Infinity in JS.

Next, look at the smallest positive number and the largest negative number. As mentioned above, under denormalization, when the exponent value is 0 and the significant digit value is not 0, it means a number that is infinitely close to 0;
At this time, the value of E = 000 00000001-011 11111111 (offset) + 1 = -100 00000000 (minus 1 and negate) = -1022, the minimum value of the exponent is 2^-1022 = 2.2250738585072014e-308;
The minimum value of significant digits that can be taken as non-zero is 0.00000000 00000000 00000000 00000000 00000000 00000000 0001 = 2^-52
The smallest positive value that can be obtained is 2^-1022 * 2^-52 = 2^-1074 = 5e-324;
And JS's Number.MIN_VALUE = 5e-324; it is exactly the same as we calculated;

5.3 Industry solutions

Rounding exactly
Banker rounds
……


chenjsh36
737 声望44 粉丝