javascript小数的二进制存储

免责声明:
我已经尽量简化了,由于观看本节内容导致的头晕、脱发、恶心、呕吐等生理症状,本人概不负责
在现实世界中,小数的书写方式非常自然,只需要使用.即可代表之后的数字全是小数,例如3.1415926
但是计算机存储小数的时候,麻烦就来了。
比如3.14,整数部分的二进制是11,小数部分的二进制是1011,合在一起就是111011,你能说这个数是3.14吗?
问题的根源就在它难以准确的表述小数点的位置。
因此,必须另寻他法。
浮点数基本概念
聪明的人想出了一个巧妙的办法
在现实世界中,任何数字都可以表示为a.xxxx * 10^na.xxxx∗10n,其中,a的取值范围是1~9
这叫做科学计数法
比如:
1024.5678 = 1.0245678 * 10^31024.5678=1.0245678∗103,它表示,1.0245678的小数点向右移动3位,即最终的十进制小数
952.7 = 9.527 * 10^2952.7=9.527∗102,它表示,9.527的小数点向右移动2位,即最终的十进制小数
那二进制是否也可以这样表示呢?
当然可以,在二进制的世界中,任何数字(包括整数)都可以表示为a.xxxx * 2^na.xxxx∗2n,a只能取1
比如:
110.101 = 1.10101 * 2^2110.101=1.10101∗22,它表示,1.10101的小数点向右移动2位,即最终的二进制小数
0010.00011 = 1.000011 * 2^10010.00011=1.000011∗21,它表示,1.000011的小数点向右移动1位,即最终
的二进制小数
可以看出,二进制如果也使用科学计数法,以下东西都是固定的:
底数2
整数部分1
而不固定的部分是:
指数部分
尾数部分(小数点后面的部分)
因此,我们可以使用下面的方式来表示一个数字
第一部分第二部分第三部分
符号    阶码    尾数    
0为正,1为负    这部分表示指数部分    这部分表示小数点后面的部分    
这种表示数字的方法,叫做浮点数表示法
比如,110.101 = 1.10101 * 2^2110.101=1.10101∗22,改数字的符号是0,阶码是2,阶码的二进制格式是10,尾数是10101,因此,在计算机中可以用浮点数表示为:
符号阶码尾数
0    10    10101    
是不是很简单。
但这样一来,容易导致CPU搞不清楚阶码和尾数是在哪里分割的,我们可以轻松的从表格中看出,但计算机哪有什么表格,它是存在一起的:01010101
为了解决这个问题,阶码和尾数的长度就必须固定
比如,阶码的长度规定为3,尾数的长度规定为4,加上一个符号位,刚好是8位,一个字节
如果按照这种约定,计算机就可以轻松的取出第一个符号位,然后轻松的取出后三位阶码,继续取出后四位的尾数
符号(1)阶码(3)尾数(4)
0    010    1010    
可以看到,这种情况下,尾数的最后一位被丢弃了,从10101变成了1010,因为它只能存储4位。
所以,使用浮点数存储数字时,可能导致存储的数字不精确
以上,就是浮点数存储数字的方式。
数字到浮点数的转换
我们知道,二进制科学计数法是浮点数的基石,只要有了二进制的科学计数法,就可以变成浮点数的存储了。
然而,我们平时接触更多的是十进制的小数
现在的问题是:如何把十进制的小数转换为浮点数的科学计数法?
下面将一步一步进行分析
二进制小数到十进制小数
要理解十进制小数是如何转换成二进制小数的,就必须要先理解相反的情况:二进制小数是如何转换成十进制小数的。
我们知道,任何一个十进制的小数(包括整数)都可以书写为下面的格式:
21.25 = 2 * 10^1 + 1 * 10^0 + 2 * 10^{-1} + 2 * 10^{-2}21.25=2∗101+1∗100+2∗10−1+2∗10−2
二进制的小数也可以用同样的规则,只不过把底数10换成底数2
下面的示例就是把一个二进制小数11.01转换成了十进制小数3.25:
11.01_2 = 1 * 2^1 + 1 * 2^0 + 0 * 2^{-1} + 1 * 2^{-2} = 3.25_{10}11.012=1∗21+1∗20+0∗2−1+1∗2−2=3.2510
十进制小数到二进制小数
知道了二进制小数转十进制,反过来也是一样的
省略了具体的数学推导(数学好的朋友自行完成),我们按照下面的方式来转换
比如十进制数3.25
首先转换整数部分:
3_{10} = 11310=11
整数部分的转换在之前的章节已经说的很详细了,不再重复
然后转换小数部分
现有小数乘以2取整数部分
0.25    0.5    0    
0.5    1    1    
0    不再处理    不再处理    
最终得到的二进制小数部分是01,即把每次取整部分从上到下依次罗列即可
0.25_{10} = 0.01_20.2510=0.012
把最终的整数部分加入进去,就形成了
3.25_{10} = 11.01_{2}3.2510=11.012
无法精确转换
有的时候,这种转换是无法做到精确的
比如0.3这个十进制数,转换成二进制小数按照下面的过程进行
现有小数乘以2取整数部分
0.3    0.6    0    
0.6    1.2    1    
0.2    0.4    0    
0.4    0.8    0    
0.8    1.6    1    
0.6    1.2    1    
0.2    0.4    0    
0.4    0.8    0    
0.8    1.6    1    
0.6    1.2    1    
...    ...    ...    
0.3_{10} = 0.0 1001 1001 1001 1001 ... = 0.0\overline{1001}0.310=0.01001100110011001...=0.01
在转换的过程中,可能导致十进制的现有小数永远无法归零,于是转换成了一个无限的二进制小数。
同时,计算机无法存储一个无限的数据,因此,总有一些数据会被丢弃,这就造成了计算机存储的小数部分可能是不精确的
进一步,如果一个小数无法精确的存储,那么他们之间的运算结果也是不精确的
这就是计算机对小数的运算不精确的原因
// js语言中运行5.3 - 5.2 // 得到0.09999999999999964
转换成二进制的科学计数
现在,按照以上所述的规则,我们已经可以轻松的把一个十进制的数字转换成二进制格式了
然后,我们再在它的基础上,把它变化为二进制的科学计数格式
3.25_{10} = 11.01_2 = 1.101 * 2^13.2510=11.012=1.101∗21
注意,1.101 * 2^11.101∗21是二进制的科学计数表示,你并不能把它当成十进制的方式运算,如果你要将其转换成十进制,应该:
将1.101的小数点向右移动1位,得到11.01
$11.01 = 1
2^1 + 1
2^0 + 0
2^{-1} + 1
2^{-2} = 3.25$
当我们拿到这个数的二进制科学计数后,就可以轻松的将其存储下来了
3.25 = 11.01 = 1.101 * 2^1 = 1.1010 * 2^13.25=11.01=1.101∗21=1.1010∗21的存储
因为尾数是4位,所以不足在后面补0
符号(1)阶码(3)尾数(4)
0    001    1010    
然而
请允许我做一个悲伤的表情
还有一种情况没有考虑到...
指数偏移量
建议先读完本节内容,然后再反复推理和思考
我们来聊一聊还有什么情况没有考虑到
现在,我们有一个十进制的数字0.25,它转换成二进制的格式应该是0.01,科学计数法表示的结果是1*2^{-2}1∗2−2,即小数点应该向左移动2位
现在的问题是,指数部分出现了负数!
注意,不是数字是负数,是指数是负数
问题在于,我难道对指数也要使用一个符号位来处理负数的情况吗?
实际上没有必要,在计算机运算浮点数时,对于指数部分,更多的操作是比较,即比较两个指数哪个大
如果使用符号位的话,在比较时就必须考虑符号的问题,这样会给比较带来很多麻烦
因此,IEEE 754规定,使用指数偏移量来处理这个问题
IEEE 754是对浮点数存储、运算的国际标准,绝大部分计算机语言的浮点数都遵循该标准
它规定,如果一个浮点数的指数位数为ee,则它的指数偏移量为2^{e - 1} - 12e−1−1,不管存储什么指数值,都需要加上这个偏移量后再进行存储。
比如,指数的位数是3,则指数的偏移量为2^{3-1} - 1 = 323−1−1=3,当存储指数-2时,需要加上偏移量3再进行存储,因此,指数-2实际上存储的是1,即001
再比如,当存储指数2时,需要加上偏移量3再进行存储,因此,指数2实际上存储的是5,即101
如果比较-2和2哪个大,就直接比较两个二进制即可,001显然比101要小,指数部分完全没有符号位,这样比较起来就轻松多了。
当然,当需要还原它的真实指数时,只需要减去偏移量即可
于是,有了这样的规则后:
0.25_{10} = 0.01_2 = 1.0000 * 2^{-2}0.2510=0.012=1.0000∗2−2
符号(1)阶码(3)尾数(4)
0    001    0000    
3.25_{10} = 11.01_2 = 1.1010 * 2^13.2510=11.012=1.1010∗21
符号(1)阶码(3)尾数(4)
0    100    1010    
由于有了偏移量的存在,浮点数的指数范围就可以很轻松的算出来了
最小能存储的指数是000,减去偏移量后真实的指数是-3
最大能存储的指数是111,减去偏移量后真实的指数是4
稍微的总结一下,就是:
阶码为n位的浮点数,指数的真实范围是-2^{n-1}+1−2n−1+1 到 2^{n-1}2n−1
特殊值
在浮点数的标准中,有下面两个特殊值:
NaN:Not a Number,表示不是一个数字,它通常来自一些错误的运算,比如
3.14 * "你好"
Infinity:正向的无穷大,相当于数学中的
\infty
∞
-Infinity:负向的无穷大,相当于数学中的
-\infty
−
∞
为了表示这三个值,IEEE 754标准规定,当使用一种特殊的数字存储来表示:
NaN
符号(1)阶码(3)尾数(4)
无所谓    
比如:
符号(1)阶码(3)尾数(4)
0    111    1010    
上面这个数字可不是1.1010*2^{4}1.1010∗24,它是一个NaN
无穷
符号(1)阶码(3)尾数(4)
0:正无穷,1:负无穷    111    
比如:
符号(1)阶码(3)尾数(4)
0    111    0000    
上面这个数字可不是1.0000*2^{4}1.0000∗24,它是一个Infinity
由于特殊值的存在,让阶码的最大值用于表示特殊值,因此,正常的数字阶码是不能取到最大值的
因此,正常数字的阶码取值范围少了一个:-2^{n-1}+1−2n−1+1 到 2^{n-1} - 12n−1−1
比如,3位的阶码,它能表示的正常指数范围是-3到3
单精度和双精度
很多计算机语言中都有单精度和双精度的概念,它们的区别在于阶码和尾数的位数是不一样的
Java的float是单精度浮点数,double是双精度浮点数
JS的所有数字均为双精度浮点数
类型符号阶码尾数共计
单精度    1位    8位    23位    32bit,4byte    
双精度    1位    11位    52位    64bit,8byte    
总结
由于浮点数这种特别的存储方式,因此,不同的阶码和尾数的位数,决定了:
阶码位数越多,可以取到的指数越大,因此可以表示的数字越大
尾数位数越多,可以表示的数字位数越多,因此可以表示的更加精确


您可能还会对下面的文章感兴趣: