5
头图

Surface work

In daily work and study, I often probe my own bottom line, whether the computer foundation is good or not, it can completely determine a person's code level and bug occurrence rate. I believe you have all learned this knowledge, but you will forget it if you don’t use it for a long time. Today I will take you to review it.

Based on the principle of easy to understand, I will this topic clearly today.

Let’s talk about this very common problem, why 0.1 + 0.2 !== 0.3 . Before officially introducing this problem, we need to understand the following
Pre-knowledge.

  • What is the form of computer binary and how is it calculated?
  • What are the original code, complement code, inverted code, and shift code, and what are they used for?

Almost these few are enough to understand this regular 0.1 + 0.2 !== 0.3 problem.

The first pre-knowledge, binary

We know that in daily life, there are many kinds of data display, including the decimal system that we routinely use in our daily life, the hexadecimal system that represents colors in css, and the binary system that performs operations in computers.

Binary representation

The calculations in the computer are all calculated in the form of binary, that is, all 0 or 1 are used to represent numbers. Let's take the decimal system as an example, such as:

  • The decimal 1 is represented as 1 in the computer
  • The decimal 2 is represented as 10 in the computer
  • The decimal 8 is represented as 1000 in the computer
  • The decimal 15 is represented as 1111 in the computer

Binary calculation method

For the binary calculation method, we divide it into two cases, one is the calculation of integers, and the other is the calculation of decimals.

Binary calculation of integer part

We first explain how to convert decimal to binary. The way to convert decimal to binary is called " divided by 2 and remainder ", that is, a decimal number is always divided by 2 to get the remaining digits. Give two examples

30 % 2 ········· 0
15 % 2 ········· 1
 7 % 2 ········· 1
 3 % 2 ········· 1
 1 % 2 ········· 1
 0

The binary conversion of an integer is read from bottom to top at , so the binary representation of 30 is 11110 .

100 % 2 ········· 0
 50 % 2 ········· 0
 25 % 2 ········· 1
 12 % 2 ········· 0
  6 % 2 ········· 0
  3 % 2 ········· 1
  1 % 2 ········· 1
  0

The binary conversion of an integer is read from bottom to top by , so the binary representation of 100 is 1100100 .

I also wrote a function to convert this binary.

function getBinary(number) {
  const binary = [];
  function execute(bei) {
    if (bei === 0) {
      return ;
    }
    const next = parseInt(bei / 2, 10);
    const yu = bei % 2;
    binary.unshift(yu);
    execute(next);
  }
  execute(number);
  return binary.join('');
}
console.log(getBinary(30)); // 11110
console.log(getBinary(100)); // 1100100

Next, let's take a look at how to convert binary to decimal. In layman's terms, each binary number is multiplied by the corresponding power of 2 from right to left and incremented. For example, take the example of 100 above. The binary representation of 100 is 1100100 , what we need to do is:

1100100
= 1 * 2^6 + 1 * 2^5 + 0 * 2^4 + 0 * 2^3 + 0 * 2^2 + 0 * 2^1 + 0 * 2^0
= 100

It's simple and clear. Needless to say, let's take a look at the implementation code:

function getDecimal(binary) {
  let number = 0;
  for (let i = binary.length - 1; i >= 0; i--) {
    const num = parseInt(binary[binary.length - i - 1]) * Math.pow(2, i);
    number += num;
  }
  return number;
}
console.log(getDecimal('11110')); // 30
console.log(getDecimal('1100100')); // 100

Binary calculation of fractional part

The binary calculation of the fractional part is different from the binary calculation of the integer part. The calculation method of converting a decimal number into a binary decimal number is called " multiplying two rounding method ", that is, multiplying a decimal number by 2 and then taking the integer part. , Until the fractional part is 0. Look at an example:

0.0625 * 2 = 0.125 ········· 0
 0.125 * 2 = 0.25  ········· 0
  0.25 * 2 = 0.5   ········· 0
   0.5 * 2 = 1.0   ········· 1

And the reading direction of the fractional part is also different. The binary conversion of decimals is read from top to bottom by , so the binary representation of 0.0625 is 0.0001 , which is exactly the case that can be divided, and in many cases it is inexhaustible, such as 0.1 and 0.2 in the title. Write a function conversion:

function getBinary(number) {
  const binary = [];
  function execute(num) {
    if (num === 0) {
      return ;
    }
    const next = num * 2;
    const zheng = parseInt(next, 10);
    binary.push(zheng);
    execute(next - zheng);
  }
  execute(number);
  return '0.' + binary.join('');
}
console.log(getBinary(0.0625)); // 0.0001

Then try to convert binary decimals to decimal decimals. Because the above is multiplication, here is division. Binary division can also be expressed as multiplication of negative exponential powers, such as 1/2 = 2^-1 ; let’s see how 0.0001 is converted to 0.0625:

0.0001
= 0 * 2^-1 + 0 * 2^-2 + 0 * 2^-3 + 1 * 2^-4
= 0.0625

Use functions to implement this form.

function getDecimal(binary) {
  let number = 0;
  let small = binary.slice(2);
  for (let i = 0; i < small.length; i++) {
    const num = parseInt(small[i]) * Math.pow(2, 0 - i - 1);
    number += num;
  }
  return number;
}
console.log(getDecimal('0.0001')); // 0.0625

We first understand this part of binary conversion. For 0.1 + 0.2 !== 0.3 , the above binary part is basically enough. Of course, the code part is for reference only, and the boundary and other issues are not dealt with...

Make a question to consolidate it:
<details>
<summary> What is the binary representation of 18.625? ? ? => Click to view details </summary>
<pre>

18的二进制表示为: 100010
0.625的二进制表示为: 0.101
所以18.625的二进制表示为:100010.101

</pre>
</details>
<sammry></sammry>

The second pre-knowledge, computer code

We know that computers use binary to perform calculations. When it comes to computer code, you have to mention IEEE standard , and operations involving fractional parts have to mention IEEE binary floating point arithmetic standard. (IEEE 754) . Its standard binary representation is

V = (-1)^s * M * 2^E
  • Where s is the sign bit, 0 is a positive number, and 1 is a negative number;
  • M is the mantissa, which is a binary decimal, which stipulates that the first digit can only be 1, 1 and the decimal point is omitted ;
  • E is the exponent, or called the order code

Why should 1 and decimal places be omitted? Because all the first digits are 1, you can add one more digit at the end to increase the accuracy. If the first digit is 0, it has no meaning.

Generally speaking, current computers support two types of precision calculation floating point formats. One is single precision (float) and the other is double precision (double).

FormatSign bitmantissaOrder codeTotal number of digitsOffset value
Single precision182332127
Double precision11152641023

Take JavaScript as an example. Double-precision format is used for calculation in js, and its floating-point number is 64 bits.

Original code

What is the original code, the original code is the simplest, that is, the sign bit plus the absolute value of the truth value, that is, the first bit is used to represent the sign, and the remaining bits represent the value. We use 11 bits as follows:

  • +1 = [000 0000 0001] original
  • -1 = [100 0000 0001] original
    Because the first bit is the sign bit, its value range is [111 1111 1111, 011 1111 1111] = [-1023, 1023];

    One's complement

    What is an inverted code? The inverted code is reversed on the basis of the original code. The opposite of a positive number is itself; the inverse of a negative number is that the sign bit remains unchanged, and the remaining bits are inverted.

  • +1 = [000 0000 0001] original = [000 0000 0001] reverse
  • -1 = [100 0000 0001] original = [111 1111 1110] reverse

Complement

What is the complement code, the complement code is the complement on the basis of the complement. The complement of a positive number is itself, and the complement of a negative number is based on its inverse, plus 1.

  • +1 = [000 0000 0001] Original = [000 0000 0001] Reverse = [000 0000 0001] Complement
  • -1 = [100 0000 0001] original = [111 1111 1110] reverse = [111 1111 1111] complement
    Why is there such a thing as complement code?
  • First of all, there is no subtraction in computers, all are additions. For example, 1-1 is 1 + (-1) in computers.
  • If you use the original code for subtraction:

    1 + (-1) = [000 0000 0001]原 + [100 0000 0001]原 
             = [100 0000 0010]原 
             = -2

    ===>>> Conclusion: wrong

  • In order to solve this wrong problem, there is an inverse code to do the subtraction:

    1 + (-1) = [000 0000 0001]反 + [111 1111 1110]反 
             = [111 1111 1111]反 
             = [100 0000 0000]原 
             = -0

    It is found that the value is correct, but the sign bit is wrong; although +0 and -0 are the same in understanding, the sign of 0 is meaningless, and there will be [000 0000 0000] and [100 0000 0000]. Kind of encoding.

    ===>>> Conclusion: not

  • In order to solve the problem caused by the above symbol, there is a complement to do the subtraction:

    1 + (-1) = [000 0000 0001]补 + [111 1111 1111]补 
             = [000 0000 0000]补 
             = [000 0000 0000]原 
             = 0

    The result obtained in this way is perfect, 0 is represented by [000 0000 0000], and the above [100 0000 0000] will not appear.

    ===>>> Conclusion: perfect

Frameshift

The code shift is obtained by inverting the sign bit of the complement code. generally used as the order code floating point number. The purpose of introducing it is to ensure that the machine zero of the floating point number is all 0. This is a plus or minus.

  • +1 = [000 0000 0001] original = [000 0000 0001] reverse = [000 0000 0001] complement = [100 0000 0001] shift
  • -1 = [100 0000 0001] original = [111 1111 1110] reverse = [111 1111 1111] complement = [011 1111 1111] shift
    Be careful to find the pattern:
  • +1 = [000 0000 0001] original = [100 0000 0001] shift
  • -1 = [100 0000 0001] original = [011 1111 1111] shift

Why 0.1 + 0.2 !== 0.3?

Back to our topic, let's look at why 0.1 + 0.2 !== 0.3 . Look at the binary representation of 0.1 and 0.2.

0.1 = 0.0 0011 0011 0011 0011 0011 0011 0011 0011 0011 .... 0011无限循环
0.2 =  0. 0011 0011 0011 0011 0011 0011 0011 0011 0011 .... 0011无限循环

It can be seen that both 0.1 and 0.2 are binary decimals with an infinite loop of 0011.

We know from the above that floating-point numbers in JavaScript are represented by 64 bits, so how are 0.1 and 0.2 represented in computers?

0.1 = (-1)^ 0 * 1.1 0011 0011 0011 * 2^(-4)
-4 = 10 0000 0100

According to IEEE 754 standard can know:

V = (-1)^S * M * 2^E
S = 0  // 1位,正数为0,负数为1
E = [100 0000 0100]原 // 11位
  = [111 1111 1011]反 
  = [111 1111 1100]补 
  = [011 1111 1100]移 
M = 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 // 52位(其中1小数点省略)

In the same way, it can be seen that the expression of 0.2:

0.2 = (-1)^ 0 * 1.1 0011 0011 0011 * 2^(-3)
-4 = 100 0000 0011

V = (-1)^S * M * 2^E
S = 0  // 1位,正数为0,负数为1
E = [100 0000 0011]原 // 11位
  = [111 1111 1100]反 
  = [111 1111 1101]补 
  = [011 1111 1101]移
M = 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 // 52位(其中1小数点

When the two are added together, the order codes are not the same, and we need to perform the order operation.

Right order

For the order, there will be cases where the mantissa moves.

  • If the big order code is aligned with the small order code, it is necessary to move the mantissa of the big order code to the left. At this time, it is possible to remove the high part of the mantissa during the shifting process, which causes the data to be corrupted. Mistake. This is not advisable
  • If the small order code is aligned with the larger order code, you need to move the number of the small order code to the right, and add 0 to the high order; this will squeeze out the data on the right, which will affect the accuracy of the data. But it will not affect the overall size of the data.

The computer adopts the latter, underestimating the big method. This is the reason for the lost.

So next, let's take a look at this move above.

// 0.1
E = [100 0000 0011]原 = [111 1111 1100]反 = [111 1111 1101]补 // 11位, 对阶后
M = 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 // 移动前
M = 0100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 // 移动后
// 0.2 保持不变
E = [100 0000 0011]原 = [111 1111 1100]反 = [111 1111 1101]补 // 11位,不变
M = 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 // 52位,不变

The above is the result of the order of 0.1 and 0.2 in binary. It is more troublesome for us to calculate this number, so we directly calculate the true value of 0.1 and 0.2.

Truth calculation

0.1 = 0.0 0011 0011 0011 0011 0011 0011 0011 0011 0011 .... 0011无限循环
0.2 = 0. 0011 0011 0011 0011 0011 0011 0011 0011 0011 .... 0011无限循环
0.1 + 0.2
    = 0.0001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 (1001...舍弃)
    + 0.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 (0011...舍弃)
    = 0.0100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 (1100...舍弃)
    = 0.2999999999999998

This is so wrong! ! !

The value we get when running in the browser is:

0.1 + 0.2 = 0.30000000000000004

The reason for the above problem is that when the computer calculates, there will be rounding processing.
As seen above, the value discarded after the truth value calculation is 1100, and there will still be rounded to 1 in the computer, which is as follows:

0.1 + 0.2
    = 0.0001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 (1001...舍弃)
    + 0.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 (0011...舍弃)
    = 0.0100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 (1100...舍弃)
    = 0.0100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1101 (入1)
    = 0.30000000000000004

At this point, we will understand this part, if there is something wrong, please point it out. Thanks for reading.

pay attention to

Welcome everyone to pay attention to my public [Delai asks the front end], the article is first published on the public account.

In addition to daily collection of community-selected articles, we also share technical articles from time to time.

Hope to learn together and make progress together.


德莱问前端
122 声望588 粉丝