本文主要简述了计算机中数字的表示方式,以及IEEE-754标准的由来及具体规定,最后简单的叙述了浮点数的运算规则。阅读这篇文章之前,最好有关于有关于机器数中的原码、反码、补码三种形式的一些概念。
计算机内数据和指令都是由晶体管和门电路等元件完成的,对于这些元件来说,开
或者关
是其唯一的状态,这种状态的表现就是二进制的理念。就像黑客帝国世界中漫天飞的0
和1
一样,计算机世界使用的机器语言也只有0
和1
。而在机器语言中,当计算机想要表示一个数字时,这时就得使用机器数了,机器数所表示的真实数值称为真值。
机器数(computer number)是将符号"数字化"的数,是数字在计算机中的二进制表示形式。机器数有2个特点:一是符号数字化,二是其数的大小受机器字长的限制
机器数有两个特点:
0
代表正数,1
代表负数。-127
`+127`,**机器数**即`11111111`01111111
。那么我们以8bit二进制为例,看看机器数原码如何表示10进制中的3
和-3
。
对于正数符号位为0,所以机器数为00000011
对于负数符号位为1,所以机器数为10000011
在上文中,我们知道计算机通过二进制可以精确的表达整数,那么计算机是如何表达小数,或者说计算机是如何处理这些小数的呢?
在早期,计算机表示小数的方式是定点小数的方式去表示的,定点小数中。小数点隐含在第一位编码和第二位中间。
如真值为正值的小数0.101,使用定点小数在计算机中表示就为0 101
然而,早期使用定点小数无法表示过大和过小的值,并且在计算过程由于数值范围的限定,会出现数值溢出的问题。
随着技术的更新,在1978年的时候,Intel公司推出了首枚16bit微处理器(CPU)8086。这台x86的老祖宗虽然自身无法处理小数的运算,但是在编译器层面可以通过用整数指令模拟出小数的运算,不过这种运算的方式效率是非常低的。
为了解决这类问题,1980年Intel公司推出了首款x87浮点协处理器运算单元(FPU)8087,通过主板上额外的协处理器插槽,安装后不仅可以解决小数的运算问题,并且对于不同的应用,性能提升了20%~500%。
对于计算机发展来说,8087是款非常棒的FPU,但是它的意义真正体现在这款FPU的设计师之一的William Kahan教授设计了IEEE-754标准的雏形,而正是因为这套标准,我们计算机才能精准的处理小数。
1985年时,IEEE推出了IEEE 754-1985标准,随着大佬们的努力,IEEE还推出了目前的版本——IEEE 754-2008。
而我们使用的高级语言中浮点数的运算,如C、C++、JavaScript、Java都是基于这个标准而定。
组成
IEEE 754标准中,浮点格式主要分为四种类型,即单精度格式、双精度格式、扩展单精度格式和扩展双精度格式。它们的构成都由以下三部分组成:
我们以64位的双精度Double类型为例,他的构成是如下图所示。
0
表示正数,1
表示负数1
,即范围为0
到2047
。
正规化
对于十进制计算来说,小数0.0011
既可以表示成1.1 x 10-3
,也可以表示成11 x 10-4
。
但是对于机器数而言,统一的标准规定它首要需要正规化,也就是必须唯一地表示小数点的位置:
上图中b
是由0
或1
构成的二进制数,b
通常由十进制
转二进制
而来,它会自动忽略起始位置的隐藏位1.
,b
最终被存入上图中尾数部分
。p
加上移码
后的二进制数存入上图指数部分
。
对于一个非0的二进制数来说,我们可以保证尾数部分一直以
1
开始,那么我们通过把第一位的1
当做隐藏位,默认它的存在,并不把它存入尾数部分,这样就可以提高1位数据表示的精度。
移码
上述介绍中有个词:移码 。
我们知道指数
是有正值或负值的,既然有负值那么就得用最高位
来表示正负符号。也就是说如果是11位
的机器数,实际上影响数值大小的位数只有10位,并且我们可以得出十进制范围为 -1023
~1024
。
如果我们以-1023
~1024
作为该数的指数范围区间,那么为了比较数据是否在范合理围内,将不可避免的进行负值加法的运算,为了解决这个问题,大佬们提出移码
的概念。其中有两个特殊值:-1023
和1024
后面会解释。
用移码来表示阶码有以下几种好处。
1.方便比较大小和加减
2.保证浮点数的机器零为0
3.特殊值(0和max)比较容易检验
4.提高表示数据的精度
非正规化
正规化告诉我们浮点数的最左边隐藏位无论何时都是1,那么对于0
就无法表示了。大佬们通过规定如果指数部分为0时,尾数部分就再也不是1.bbbbbb
的形式了,而是0.bbbbbb
这种非正规化的表示形式,这样如果尾数部分全部为0
时,即表示该数值为0
。下图是64位双精度非正规化的表示方式:
无穷大和NaN
当指数为最小值时表示非正规化,而当指数为最大值+1
,尾数部分全部为0就表示无穷大
,当尾数不全为0就为NaN
。
机器数中的ε
ε表示1与大于1的最小浮点数之差,不同精度的浮点的ε是不同的。一般来说ε是根据不通精度的尾数部分宽度来定的。例如双精度有52位的尾数宽度p,那么该精度下ε为2-52,如下:
无小数浮点数二进制表示
首先我们通过上述结论我们可以得到以下求值公式。
(-1) ^ 符号位 * 1.bbbbb... * 2 ^ 指数部分
下面我们使用4399
这个十进制整数我们套用上述公式,反推得到以下64位2进制浮点数结果。
4399
转为2进制数1000100101111
,因为是正数,那么符号位为 0
。 然后我们根据正规化的二进制浮点数表达,那么它以1.bbbbb...
这种形式表示为1.000100101111 x 212
1
之后尾数部分为:000100101111
。12
经过偏移加上1023
换算为二进制:10000001011
。0-10000001011-00010010111100...000
最后,推荐大家一个IEEE 754 64位转换工具方便大家测试自己的运算结果。
在了解了浮点数表示的标准,与机器数编码标准后,我们来看看计算机中的浮点数是如何运算的。
浮点数运算分为两个部分,阶码运算与尾数运算,并且所有运算均采用补码运算,具体的运算一般可以分为以下五个个步骤,我将试着通过一个浮点数加减法的实际案例结合以下五个步骤进行同步说明:
假设有两组IEEE-754标准的64位双精度浮点数:
第一组x浮点数为: 0 01111111011 1001100110011001100110011001100110011001100110011010
第二组y浮点数为: 0 01111111100 1001100110011001100110011001100110011001100110011010
现在需要进行求x+y的值。
对阶
对于浮点数来说,两浮点数进行加减,首先看两数的阶码是否相同,即小数点位置是否对齐。若两数阶码相同,表示小数点是对齐的,就可以进行尾数相加减,反之,此时需要使两数的阶码相同,这个过程叫做对阶。
对于对阶来说需要注意的是:
小阶对大阶
,小阶对大阶
的好处是,当小阶不同于大阶时,只需要移除小阶数的尾数部分的低位部分,如果是大阶对小阶的话,就需要移除大阶的高位部分了,这样的误差是无法接受的。x的阶码为 01111111011
y的阶码为 01111111100
计算 01111111011 - 01111111100
转为补码计算的加法 01111111011 + (-01111111100) = 01111111011 + 10000000100[补]
得出的结果转为原码 11111111111[补] = -1[十]
由于得知x与y的阶差为-1,所以我们需要将小阶x的尾数右移一位
原x的尾数部分1001100110011001100110011001100110011001100110011010
右移后x的尾数11001100110011001100110011001100110011001100110011010
根据0舍1入原则,右移移位后,尾数补上隐藏位的1,舍去移除的0,最后得到x的尾数部分为:
1100110011001100110011001100110011001100110011001101
注意对阶完成后,不管是加减运算,此时原数的阶码已经为大阶的阶码了
尾数计算
由于已经对阶完成,我们只需要将52位尾数部分+1位隐藏进行加减计算。
如果隐藏位被右移了,那么默认补0
0.1100110011001100110011001100110011001100110011001101
+ 1.1001100110011001100110011001100110011001100110011010
————————————————————————————————————————————————————————
10.0110011001100110011001100110011001100110011001100111
10.0110011001100110011001100110011001100110011001100111
就是尾数计算后的结果。
结果规格化
根据规格化的要求,我们需要将第2部计算出的尾数右移1位,并将阶码+1
0 01111111101 0011001100110011001100110011001100110011001100110011(1)
这里计算后1隐藏位还是继续隐藏。
右移后的尾数为上面的数字,这里需要注意的是,最低位被右规了1,造成精度丢失的问题
舍入
根据0舍1入的规则,由于我们右规了1,所以要进行+1的预算。
0011001100110011001100110011001100110011001100110011
+ 0000000000000000000000000000000000000000000000000001
————————————————————————————————————————————————————————
0011001100110011001100110011001100110011001100110100
溢出判断
阶码01111111101
是没有发生上下溢出的情况的。所以舍入后的结果为最终的尾数部分。
最终我们得到IEEE-754的浮点数0 01111111101 0011001100110011001100110011001100110011001100110100
将它转换为10进制数得到0.30000000000000004440892098500626
而题目中的x和y是0.1
和0.2
。
说到这里聪明的你应该知道为什么0.1+0.2不等于0.3了吧。