IEEE754标准下的浮点数表示
徐徐 抱歉选手

在十进制中,1/2,1/4,1/5,1/8,1/10可以被准确地表示为小数,但是1/3,1/6,1/7,1/9却都是循环小数,这是为什么?十进制的base是10,10的质因数/prime factor为2和5,可以准确的表示为小数的那些数字的分母都是从10的质因数2和5衍生而来的。

同理,在二进制中,base是2,2的质因数只有2,因此1/2,1/4,1/8可以被准确地表示为小数,而1/5,1/10是循环小数。

计算机编程语言中输入的十进制数字,首先会被转化为二进制存储(怎么进行转化,这就是IEEE754规定的标准)在计算机中。由于存储空间有限,循环小数的二进制会在特定的位被舍去/截断。在舍去/截断的二进制基础上再进行各种运算,最后输出的时候又需要转换成十进制输出。因此,二进制无法在有限长度中精确地表示十进制中的0.1和0.2,因此。

这也就不难怪,在JavaScript中对Number类型进行运算结果会如下。其他类型的语言的输出结果可以查看Floating Point Math

1
2
> 0.1 + 0.2
< 0.30000000000000004

我们知道计算机无法精确表示0.1和0.2,他们的计算结果也不精确,但是为什么偏偏是0.30000000000000004呢?这就涉及到IEEE Standard for Floating-Point Arithmetic (IEEE 754)标准,它规定了CPU应该如何存储、运算、表示浮点数。

常说的单精度和双精度是什么意思?

在学C语言的时候,老师介绍数据类型,说到浮点数,就说有单精度和双精度两种类型,其实这个双精度和单精度就是IEEE 754规定的小数在CPU当中的存储方式。IEEE 754规定了四种表示浮点数值的方式:32位单精度,64位双精度,53位延伸单精度,80位延伸双精度;这样的规定优点是可以归一化处理整数和小数。

单精度,占4个byte/字节,32位。

2560px-Float_example.svg

双精度,占8个byte/字节,64位。

IEEE_754_Double_Floating_Point_Format.svg

可以看到,IEE 754标准的浮点数由三个阈组成,分别是sign bit/符号位,expoennt bias/指数偏移值,fraction/分数值。32位与64位的不同在于exponent bias和fraction的位不同,sign bit都是一位。

带小数的十进制转二进制

带小数的十进制转换为二进制,分开处理整数部分和小数部分。

对于整数部分,除2取余,逆序排列。对于小数部分,称2取整,顺序排列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
以173.8125为例子
整数部分 10101101
173 / 2 = 86 ... 1
86 / 2 = 43 ... 0
43 / 2 = 21 ... 1 ↑
21 / 2 = 10 ... 1 | 逆序排列
10 / 2 = 5 ... 0 |
5 / 2 = 2 ... 1 |
2 / 2 = 1 ... 0
1 / 2 = 0 ... 1

小数部分 1101
0.8125 * 2 = 1.625 |
0.625 * 2 = 1.25 | 顺序排列
0.25 * 2 = 0.5 |
0.5 * 2 = 1 ↓

结果 10101101.1101

二进制的科学计数法,以十进制173.8125为例子,科学计数法1.738125 X 10^2,底数为10。对应二进制10101101.1101的科学计数法为1.01011011101 X 2^7,底数为2。

IEEE 754 双精度 64位

JavaScript中Number类型采用了IEEE 754 双精度标准,既可以表示整数,也可以表示小数。

64位双精度分别由:

  • 1位符号位sign,正数0,负数1
  • 11位指数偏移位exponent,范围是0~2^11-1(2047),等于固定偏移值加上指数实际值的11位二进制表示。在64位中,固定偏移值为1023。
  • 52位分数位fraction

一个浮点数(Value)就能表示为:

具体例子 0.1和0.2

以0.1和0.2为例子,将其转换为64位双精度浮点数。

第一步,将0.1转换为二进制为0.0 0011 0011 0011 0011……无限重复0011。

第二步,将0.1的二进制表示转换为科学计数法为2^-4 X 1. 1001 1001 ……重复1001无限次。

第三步,对科学计数法的0.1二进制按照IEEE 754标准转换为浮点数。

  • sign为0
  • exponent为-4+1023(64位固定值)=1019,转换为11位二进制为01111111011
  • fraction为小数点后面的52位,1001……1001(52位,13个1001)|(要从这里发生截断)1001,由于第53位是1,会产生进位,因为会变为1001……1001(48位,12个1001)1010。

最终,0.1的存储序列为

image-20210404165921322

同理,0.2的存储序列为

image-20210404170016561

精度丢失发生在哪里

通过上面0.1和0.2的双精度表示,可以发现实际上是上述两个二进制数字在做运算。每个数字表示成二进制就只有52位能够储存,后面的小数都被截断了,这是第一处精度丢失的地方。

第二处丢失精度的地方发生在两个双精度浮点数参与计算,对阶的时候。这里的对阶,就是要令参与运算的两个双精度浮点数的exponent一样的过程,exponent的值就是阶。在加法时,小的的指数域要转化为大的指数域,必然要小数点左移,一旦小数点左移,52位fraction的最左边0变多了,最右边的数字又被挤出去了,又一次精度丢失。

固定偏移值

这一部分会回答下面两个问题。

  • 固定偏移值怎么计算?

  • 为什么要加上固定偏移值?

固定偏移值的计算方法为$2^{e-1}-1$,$e$是存储指数的比特长度。在双精度表示中,是11位,因此固定偏移值位1023。在单精度表示中,是8位,因此固定偏移值为128。

在双精度浮点数中,exponent的存储范围位1~2046(0和2047有特殊意义),减去1023的偏移,实际范围位-1022~+1023。

由于实际指数值可能为负,所以11比特中需要有一位拿来表示符号,虽然补码(two’s complement)常用来表示带符号的二进制,但会使二进制数字比较大小很麻烦。因此将expoent存储为unsigned value来使比较大小简单一些。

The exponent field is an 11-bit unsigned integer from 0 to 2047, in biased form: an exponent value of 1023 represents the actual zero. Exponents range from −1022 to +1023 because exponents of −1023 (all 0s) and +1024 (all 1s) are reserved for special numbers.

为什么选1023而不是1024?11位就是2^11,可以表示2048个数,劈开一半作偏移,就是1024,但是因为要去掉有特殊作用0和2048,就剩下2046/2=1023了,因此1023作偏移。

为什么console.log(0.1)输出就是0.1

我们知道,0.1在储存的时候就是丢失了精度的,因为他转换为2进制是是无限循环的,之所以得到0.1是因为console.log()在输出的时候JavaScript自动做了截断。

双精度浮点数输出的截断按照如下规则:

The 53-bit significand precision gives from 15 to 17 significant decimal digits precision (2−53 ≈ 1.11 × 10−16). If a decimal string with at most 15 significant digits is converted to IEEE 754 double-precision representation, and then converted back to a decimal string with the same number of digits, the final result should match the original string. If an IEEE 754 double-precision number is converted to a decimal string with at least 17 significant digits, and then converted back to double-precision representation, the final result must match the original number.

因为64位双精度浮点数的表示能力最多到十进制小数点的第十六位,因此

  • 如果十进制小数点位数至多15位,不会截断,按照样子输出。

  • 如果十进制小数点位数至少17位,最终以转化为到二进制的53位为止的双精度浮点数。

1
2
3
4
5
6
7
8
9
10
> 0.1 == 0.1000 0000 0000 001(15)
< false
> 0.1 == 0.1000 0000 0000 0001(16)
< false
> 0.1 == 0.1000 0000 0000 0000 1(17)
< true
> 0.1 == 0.1000 0000 0000 0000 01(18)
< true
> 0.1 === 0.1000 0000 0000 0000 01(18)
< true

在编程中处理双精度浮点数

在处理双精度浮点数相关的编程时,不要使用相等,而应该设置一个容许的误差范围,因为双精度浮点数的运算本就不能完全精确。

不要使用if(x==y) {...}来比较两个浮点数,而是使用if (abs(x-y) < myToleranceValue) {…},一般都会把这个myToleranceValue称作epsilon,注意这个epsilon和语言中内置的epsilon constants是不一样的。

参考

Floating Point Math

为什么 0.1 + 0.2 = 0.300000004

javascript 双精度浮点数剖析

该死的IEEE-754浮点数,说「约」就「约」,你的底线呢?以JS的名义来好好查查你

Double-precision floating-point format

单精度与双精度是什么意思,有什么区别? - 叛逆者的回答 - 知乎

IEEE 754浮点数标准中64为浮点数为什么指数偏移量是1023?

IEEE754的可视化

  • 本文标题:IEEE754标准下的浮点数表示
  • 本文作者:徐徐
  • 创建时间:2021-04-04 14:26:14
  • 本文链接:https://machacroissant.github.io/2021/04/04/floating-point-arithmetic/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
 评论