本节书摘来自华章计算机《计算机系统:核心概念及软硬件实现(原书第4版)》一书中的第3章,第3.5节,作者:[美] J. 斯坦利·沃法德(J. Stanley Warford)著, 更多章节内容可以访问云栖社区“华章计算机”公众号查看。
3.5浮点数表示
本章前面几节描述的数值表示是对于整数值的。C++有3种数值类型有小数部分:
float 单精度浮点数
double 双精度浮点数
long double 长双精度浮点数
这些类型的值在ISA3层不能以补码的形式存储,因为存储必须提供存放数字中小数点位置的方式。浮点数值用科学计数法的二进制版本来存储。
3.5.1二进制小数
二进制小数有一个二进制小数点,它是十进制小数点的二进制版本。
例3.33图3-25a展示了101.011(bin)的位置值。二进制小数点左边的位与图3-2无符号二进制表示中相应的位有相同的位置值。二进制小数点右边的位置值从1/2开始,每个位置值是前一位的一半。图3-25b给出的加法表明得到的值是5.375(dec)。 □
图3-26是有小数部分数字的多项式表示。小数点左边一位的位置值总是基数的0次方,即1。往左下一个有效位是基数的1次方,即基数本身。小数点右边一位的位置值是基数的-1次方,往右下一个有效位是基数的-2次方,右边每个位置值是它左边位位置值的1/基数倍。
确定二进制小数的十进制值分为两步。首先,用例3.3中无符号二进制数转换的方法转换二进制小数点左边的位。然后,用逐位翻倍的算法转换二进制小数点右边的位。
例3.34图3-27展示了把6.585 937 5(dec)转化为二进制的过程。转换整数部分就是转化小数点左边的110(bin);转换小数部分是把小数点右边的数字写在表格右列的头部,小数部分乘以2,小数点左边的数字写在左列,小数部分写在右列。下次乘2时,不包括整数部分。例如,把.171 875乘2得到0.343 75,而不是把1.171 875乘2。左列从上到下的数字就是二进制小数部分从左到右的位,因此6.585 937 5(dec)= 110.100 101 1(bin)。 □
把小数部分从十进制转换到二进制的算法就像是把整数部分从十进制转换到二进制算法的镜像。图3-5给出了用逐位除以2的算法转换十进制整数的过程。除法的余数就是得到的数字位,顺序是从二进制小数点开始从右往左。用逐位乘以2的算法转换小数部分,乘法得到的整数部分是生成的数字位,顺序是从二进制小数点开始从左往右。
一个可以用有限位十进制表示的数,它的二进制数表示可能是无限位的。
例3.35图3-28展示的是把0.2(dec)转换为二进制的过程。第一次乘2得到0.4,反复几次后又得到了0.4。显然这个过程不会终止,所以0.2(dec)=0.001100110011...(bin),位模式0011会不断重复。 □
由于所有计算机单元都只能存储有限的位,所以0.2(dec)不能被精确存储,必定是一个近似值。我们应该意识到由于二进制表示值的固有舍入误差,所以如果用像C++这样的HOL6层语言做加法0.2 + 0.2,也许不会精确得到0.4。正是由于这个原因,好的数值软件几乎不会检测两个浮点数是否完全相等,而是用软件维护了一个很小的非零容忍值,用以表示如果两个浮点数的差小于该值就被看作相等。如果容忍值是0.0001,那么1.382 64和1.382 67会被认为是相等的,因为它们的差0.000 03小于容忍值0.0001。
3.5.2余码表示
可以用常见于十进制数的科学计数法的二进制版本来表示浮点数。一个以科学计数法表示的非零数是规格化的,如果它的第一个非零位正好在小数点左边。因为0没有第一个非零位,因此0不能被规格化。
例3.36十进制数-328.4的科学计数法规格化表示是-3.284×102,10的2次方的作用是把小数点往右移动2位。类似地,二进制数-10101.101的科学计数法规格化形式是-1.0101101×24,2的4次方的作用是把二进制小数点往右移动4位。 □
例3.37二进制数0.00101101的科学计数法规格化表示是1.01101×2-3,2的-3次方的作用是把二进制小数点往左移动3位。 □
一般来说,浮点数可以是正数或负数,它的指数也可以是正整数或负整数。图3-29展示了存储浮点数值的一个内存单元。单元分为3个字段,第一个字段1位,用于存储该数的符号,第二个字段存储代表规格化二进制数的指数位,第三个字段称为有效位数,存储代表数值大小的位。
任何有符号整数的表示方法都可以用于存储指数。你可能会想到用补码表示,因为大多数计算机存储有符号整数都用它。但是实际上没有用补码,而是用了有偏差的表示方法,后面很快会解释这个原因。
一个5位单元有偏差表示的例子是余15码(Excess 15)。单元存储数字的范围十进制表示为-15到16,二进制表示为00000到11111。把十进制转换到余15码是把十进制数值加15,然后按照无符号数方式转换为二进制。从余15码转换为十进制是把它按照无符号数写作十进制数,然后减去15。在余15码中,第一位表示一个值是正还是负,不过与补码表示不一样,1表示正值,0表示负值。
例3.38把十进制5转换到余15码,5 + 15=20。然后按照无符号数方法把20转换为二进制,20(dec)=10100(excess 15)。第一位是1表示是一个正值。 □
例3.39把00011从余15码转换到十进制,把00011当作无符号值转换,00011(bin)= 3(dec),然后3 - 15=-12,因此00011(excess 15)=-12(dec)。 □
图3-30展示了一个3位单元以余3码表示和以补码表示存储整数的比较。每种表示法存储8个值,余3码的表数范围是-3到4(dec),而补码的是-4到3(dec)。 □
3.5.3隐藏位
假设浮点数以第一个非零位直接存储为小数点左边的规格化形式,那么就不需精确地存储二进制小数点,因为它总是在同样的位置。假设图3-29的符号位为1表示负值,为0表示正值,指数是3位,尾数是4位,那么可以存储尾数为4位的数字。要存储十进制值,首先把它转换为二进制,写成规格化的科学计数法的形式,然后以余3码的形式存储指数,再存储尾数的多个最高有效位。
例3.40 存储0.34,把它转换到二进制0.34(dec)=0.010101110...,这个位序列是无穷尽的,因此只存储最高位。这个数的规格化科学计数法值是1.0101110…×2-2,指数是-2。从图3-30可以看到它的余3码表示是001。最高的4位是1010,它在第一个数字的后面隐含了小数点。这个数是正数,因此符号位是0。存储这个值的位模式是0 001 1010。
来看一看这个近似值有多接近,把存储值转换回十进制。存储值1.010×2-2(bin)=0.3125,它和原始十进制值相差0.0275。 □
令人遗憾的是不能在尾数中存储更多的高位数字。当然,与实际机器中的浮点数格式相比,指数3位,尾数4位是太小了。例子采用这么小的表示是为了说明起来简单。然而,即使在实际的机器中尾数字段大得多,近似度好很多,但是仍然不可避免近似的发生,因为内存单元是有限的。
可以利用当数字用规格化表示时二进制小数点左边总是1的这个事实。因为1总是在那里,所以可以简单地不存储它,这可以给尾数扩展1位精确度的空间。这个假定在二进制小数点左边而又不显式存储的位叫作隐藏位(hidden bit)。
例3.41采用假定在尾数中有隐藏位的表示方法,0.34(dec)存储为0 001 0101。二进制小数点右边的前4位是0101,小数点左边的1位是假定的。来看看精确度的改进。现在的存储值是1.0101×2-2(bin)=0.328 125,它与原始的十进制值相差0.011 875,没有隐藏位的差是0.0275,因此使用隐藏位改进了近似值。 □
当然隐藏位是假定的,不要忽略它。当在一个程序中写十进制浮点数时,编译器生成代码把值转换为二进制,会丢弃假定存在的隐藏位,尽可能多地存储二进制小数点右边的位。如果程序把两个存储的浮点数相乘,那么计算机在执行乘法运算前,会抽取尾数位并插入假设的隐藏位。对于乘积,会除去隐藏位后才进行存储。
3.5.4特殊值
有些实际值需要特殊看待,最明显的是0,因为它的二进制表示中没有为1的位,因此它不能规格化表示,必须为它设置一个特殊的位模式。标准的做法是把指数全置为0,尾数也全置为0。符号位呢?最常见的是0有两种表示:一个正0,一个负0。如果指数3位,尾数4位,两种0的位模式是
1 000 0000(bin)=-0.0(dec)
0 000 0000(bin)=+0.0(dec)
不过,0的存储还有其他解决方案。如果+0.0的位模式没有特殊指定,那么0 000 0000会被解读成有隐藏位,看作1.0000×2-3(bin)=0.125,如果这个值没有被保留为0,那么这就是可以存储的最小正值。如果这个位模式为0保留,那么可存储的最小正值是略大的0 000 0001=1.0001×2-3(bin)=0.132 812 5。除了符号位是1,数值最小负数的尾数应该是一样。具有最小非0尾数的数应该是
1 000 0001(bin)=-0.1328125(dec)
0 000 0001(bin)=+0.1328125(dec)
可以存储的最大正整数的位模式应该具有最大指数和最大尾数,而具有最大数值大小的负数应该是一样的位模式,除了符号位为1。具有最大数值大小的位模式和它们的十进制值应该是
1 111 1111(bin)=-31.0(dec)
0 111 1111(bin)=+31.0(dec)
图3-31是0有唯一特殊值的表示方式所对应的数轴。和整数表示一样,可以存储多大的值是有限制的。如果9.5乘以12.0,两者都在范围内,但是乘积的真实值114.0在正上溢区。
然而,和整数值不一样的是,实数轴有下溢区。如果0.125乘以0.125,两者都在范围内,但乘积的真值0.156 25却在正下溢区,可以存储的最小正值是0.132 815。
当计算以确切的精度进行时,近似的浮点数的数值计算结果要和预期保持一致。例如,假设9.5乘以12.0,结果应该存储什么呢?假设把最大值31.0作为近似结果存储。再假设它是一个更长计算的中间值,然后要计算它的一半是多少,将得到15.5,这和正确的值相差甚远。
在下溢区有同样的问题。如果把0.0作为0.156 25的近似值存储,然后再把它乘以12.0,就会得到0.0。你会有被看上去合理的值误导的风险。
由上溢和下溢引起的这些问题,通过引入更多的特殊值会有所改善。与0的表示一样,必须用一些特殊的位模式来表示这些特殊值,如果这些位模式不用来表示特殊值,就可以用来表示数轴上的值。除了0以外,有3个常用的特殊值:
无穷大
非数
非规格化数
无穷大用于表示溢出区的值。如果运算结果溢出,那么就存储无穷大的位模式。如果再对这个位模式执行运算,结果就是预期的值—无穷。例如,3/∞=0,5 +∞=∞,而无穷大的平方根是无穷大。除以0会得到无穷大,例如,3/∞=∞,-4/∞=-∞。如果实数做计算得到无穷大,那么就可以知道某个中间结果发生了溢出。
如果一个值不是一个数(即,非数),它的位模式称为NaN。用NaN来表示非法的浮点运算,例如,取负数的平方根得到NaN,0/0也得到NaN。任何至少有一个NaN操作数的浮点运算都得到NaN,例如,7 + NaN=NaN。
无穷大和NaN的位模式都使用了指数的最大正值,即指数字段是全1。无穷大的尾数为全1,NaN的尾数可以是任何非零模式。把这些位模式保留给无穷大和NaN会减少可以存储的值的范围。对于3位指数和4位尾数来说,最大数值的位模式和它们的十进制值是
1 111 0000(bin)=-∞
1 110 1111(bin)=-15.5(dec)
0 110 1111(bin)=+15.5(dec)
0 111 0000(bin)=+∞
在图3-31中,上溢出区的无穷大值,没有对应的下溢区的无穷小值。不过非规格化数是特殊的值,它们有一个很好的属性,称为逐级下溢。有了逐级下溢,最小正值和0之间的差距减小了很多。主要思想是,选取那些指数字段全0的非零值,把它们平均分布在下溢区中。
因为指数字段全0是为非规格化数保留的,所以最小正规格化数是0 001 000=1.000× 2-2(bin)=0.25(dec)。如果指数字段可以是000,那么最小正规格化数是0.132 812 5,我们现在的做法似乎把事情弄得更糟了。但是,非规格化数分布在原来表示方法的下溢区间内,实际上是减小了下溢区的大小。
当指数字段全是0、尾数至少包含一个1时,要使用特殊的表示规则。假定指数是3位,尾数是4位,
假定二进制小数点左边的隐藏位为0,而不是1。
假定指数以余2码而不是余3码的形式存储。
例3.42对于3位指数、4位尾数的表示方法来说,0 000 0110表示什么十进制值?因为指数全是0,尾数至少包含一个1,所以这个数是非规格化数,它的指数是000(余2码)=0 - 2=-2,它的隐藏位是0,所以它的二进制科学计数值是0.0110×2-2 。因为这是非规格化数的特殊情况,所以指数以余2码而不是余3码表示。将这个二进制值转换为十进制,得到0.093 75。 □
这样的表示会让下溢区的表示变得更好。计算具有最小数值的数,它是非规格化的。
1 000 0001(bin)=-0.015625(dec)
1 000 0000(bin)=-0.0
0 000 0000(bin)=+0.0
0 000 0001(bin)=-0.015625(dec)
如果没有非规格化数,最小正数是0.132 812 5,因此实际上下溢区变小了很多。
图3-32展示了具有所有特殊值的3位指数和4位尾数表示法的一些重要值。这些值按照数字从小到大的顺序排列。图3-32表明为什么要用余码来表示浮点数的指数。忽略符号位,只考虑从0.0到+∞的正数。可以看到,如果把最右边的7位看作一个简单的无符号整数,那么从表示0的000 0000到表示∞的111 0000,相邻的值都是加1。如果对两个浮点数进行比较,比如这样一条C++语句
那么计算机不需要提取指数字段或者插入隐藏位,只需要把最右边7位当作整数,进行比较,就能判断哪个浮点数有更大的数值。整数运算的电路要比浮点数的快很多,因此用余码表示指数实际上提高了性能。
对于负数,也有同样的模式。可以把最右的7位看作无符号整数,用以比较负数的数值大小。如果指数用补码表示,浮点数就不会有这样的属性。
如果x值计算为-0.0,y值计算为+0.0,那么程序员会预期表达式(x3.5.5IEEE 754浮点数标准
电气电子工程师学会(IEEE)是一个由会员支持的专业协会,为各种工程领域提供服务,计算机工程就是其中之一。协会内有各种小组起草工业标准。在IEEE提出它的浮点数标准之前,每个计算机厂商都设计它们自己的浮点数值表示法,互不相同。在网络普及前的早期,计算机之间的数据共享很少,因此这种情况尚可容忍。
即便没有大量的数据共享需求,标准的缺失也阻碍了数字计算的研究和发展。在两台不同的计算机上运行两个一样的程序,同样的输入可能产生不同的结果,原因是两台计算机采用了不同的近似值表示法。
1985年IEEE设立了一个委员会来起草浮点数标准。最终产生了两个标准:854更适用于手持计算器,而754广泛应用于计算机。实际上,现在每个计算机厂商的计算机中的浮点数都遵循IEEE 754标准。
在本节前面讲述的浮点数表示法中,除了指数字段和有效位的数位不同之外,其余的都和IEEE 754是一样的。图3-33展示了这个标准的两种格式:单精度格式的指数字段是8位单元,采用余127码表示(除了非规格化数,它们用的余126码),尾数是23位;双精度格式的指数字段是11位单元,采用余1023码表示(除了非规格化数,它们用的余1022码),尾数是52位。
有下列单精度格式的位值,正无穷大是
0 1111 1111 000 0000 0000 0000 0000 0000
写成4位一组的全32位模式为
0111 1111 1000 0000 0000 0000 0000 0000
它的十六进制简化表示为7F80 0000(hex)。最大的正值为
0 1111 1110 111 1111 1111 1111 1111 1111
其值近似是2128或1038,它的十六进制表示是7F7F FFFF(hex)。最小的规格化正数是
0 0000 0001 000 0000 0000 0000 0000 0000
它的十六进制表示是0080 0000(hex)。最小的非规格化正数是
0 0000 0000 000 0000 0000 0000 0000 0001
它的十六进制表示是0000 0001(hex),近似值为10-45。
William V. Kahan
1933年,William V. Kahan生于加拿大。他曾就读于多伦多大学,1958年获得数学博士学位。
1976年,Intel计划为它的微处理器产品线构造一个浮点协处理器。John Palmer负责该项目,他说服Intel需要一个数学标准,这样公司生产的不同芯片对于相同的浮点输入就能得到相同的输出。在Stanford大学时,Palmer听说过Kahan分析了一些当时流行的计算机的浮点值表示。他雇用Kahan作为咨询专家,建立浮点数表示法的细节。
在那之后,IEEE成立了一个委员会来开发业界的浮点数标准。Kahan是该委员会的成员,虽然刚开始有一些争议,但是他在Intel的工作还是成为了IEEE 754标准的基础。当时,Digital Equipment Corporation(DEC)在它的VAX系列计算机上采用了一种广受重视的浮点数表示法。在刚开始与Palmer接触时,Kahan甚至建议Intel就采用该方法。但是VAX的表示法没有逐级下溢的非规格化数。在委员会的讨论中,因为认为这种表示法的所有实现执行起来都很慢,所以这个特性成为一个很大的问题。这场关于逐级下溢的论战持续多年,DEC声称具有逐级下溢特性的计算性能都不可能超过VAX。最后,加利福尼亚大学伯克利分校的Dave Patterson的一个研究生,George Taylor,构建了一个Kahan的浮点数标准的工作原型电路板,可以插入VAX机器而不会降低机器的速度。
本章略去了很多有关IEEE 756的细节,包括保护数字、异常和标志等方面的规定。Kahan致力于“让数值计算的世界更安全”。实际上,所有硬件都遵循该标准,只是有些软件系统没有很好地利用异常和标志。当发生这种情况时,Kahan就会很快公布这些问题。Sun Microsystems在推广Java语言时用到这样一句口号“写一次,随处运行”,就曾经被Kahan在名为“How Java抯 Floating-Point Hurts Everyone Everywhere”的论文中批评过。当Matlab软件最新发布的版本没有像较早的版本那样遵循IEEE 754标准时,Kahan的论文题目就是“Matlab抯 Loss Is Nobody抯 Gain”。
1989年,William Kahan因为他在数值分析方面的基础性贡献获得了A . M. Turing奖。在本书书写之时,他是加利福尼亚大学伯克利分校数学系和电器工程和计算机科学系的教授。
例3.43-47.25的单精度浮点数的十六进制表示是什么?整数47(dec)=101111(bin),小数0.25(dec)=0.01(bin),因此。这个数是负数,因此第一位是1,指数5通过5+127=132(dec)=1000 0100(余127)转换为余127码,尾数存储二进制小数点右边的0111101,因此位模式是
1 1000 0100 011 1101 0000 0000 0000 0000
十六进制表示为C23D 0000(hex)。 □
例3.44十六进制表示为3CC8 0000的二进制科学表示法是什么?它的位模式为0 0111 1001 100 1000 0000 0000 0000 0000,符号位是0,因此这个数是正数,指数是0111 1001(excess 127)=121(unsigned)=121-127 =-6(dec),尾数的小数点右边是1001,隐藏位为1,因此这个数是。 □
例3.45十六进制表示为0050 0000的二进制科学表示法是什么?它的位模式是0 0000 0000 101 0000 0000 0000 0000 0000,符号位是0,因此它是正数,指数字段全是0,因此它是非规格化数,指数0000 0000(excess 126)=0(unsigned)=0-126=-126(dec),隐藏位是0而不是1,因此这个数是。 □