在十进制中,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 | > 0.1 + 0.2 |
我们知道计算机无法精确表示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位。

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

可以看到,IEE 754标准的浮点数由三个阈组成,分别是sign bit/符号位,expoennt bias/指数偏移值,fraction/分数值。32位与64位的不同在于exponent bias和fraction的位不同,sign bit都是一位。
带小数的十进制转二进制
带小数的十进制转换为二进制,分开处理整数部分和小数部分。
对于整数部分,除2取余,逆序排列。对于小数部分,称2取整,顺序排列。
1 | 以173.8125为例子 |
二进制的科学计数法,以十进制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的存储序列为

同理,0.2的存储序列为

精度丢失发生在哪里
通过上面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 | > 0.1 == 0.1000 0000 0000 001(15) |
在编程中处理双精度浮点数
在处理双精度浮点数相关的编程时,不要使用相等,而应该设置一个容许的误差范围,因为双精度浮点数的运算本就不能完全精确。
不要使用if(x==y) {...}来比较两个浮点数,而是使用if (abs(x-y) < myToleranceValue) {…},一般都会把这个myToleranceValue称作epsilon,注意这个epsilon和语言中内置的epsilon constants是不一样的。
参考
该死的IEEE-754浮点数,说「约」就「约」,你的底线呢?以JS的名义来好好查查你
Double-precision floating-point format
单精度与双精度是什么意思,有什么区别? - 叛逆者的回答 - 知乎
- 本文标题:IEEE754标准下的浮点数表示
- 本文作者:徐徐
- 创建时间:2021-04-04 14:26:14
- 本文链接:https://machacroissant.github.io/2021/04/04/floating-point-arithmetic/
- 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!