当英特尔公司计划为最初的8086微处理器引入FPU(Floating-Point Unit,浮点运算单元)时,意识到良好的浮点表示形式设计需要数值分析背景,而这可能是擅长芯片设计的电气工程师和固态物理学家并不具备的。他们的做法非常明智,从公司外聘请最好的数值分析师(William Kahan)来设计8087FPU的浮点格式。然后,Kahan又聘请了该领域的另外两名专家(Jerome Coonen和Harold Stone),三人共同设计了KCS浮点标准。他们的设计如此优秀,IEEE组织直接在这种格式的基础上设计了IEEE 754浮点格式标准。
英特尔公司实际上一共引入了三种浮点格式来满足不同的性能和精度要求,三种格式分别是:单精度、双精度和扩展精度。单精度和双精度格式相当于C语言的float和double类型,或者 FORTRAN的real和double precision类型。扩展精度比双精度还要多出16位,在保存结果被舍入为双精度浮点值之前,这些位可以作为连续浮点计算中的保护位。
单精度浮点格式的尾数有24位,阶码有8位。尾数的值在1.0~2.0(不包括2.0)的范围内。尾数最高位始终为1,表示二进制小数点左边的数字。剩余的23位尾数表示二进制小数点右边的值:
高位隐含为1意味着尾数始终大于等于1。即使尾数其他位全部为0,高位隐含的1也能让尾数的值始终为1。二进制小数点右边的每一位都代表一个值(0或1)乘以2的负数次幂,指数按位递增,但即使二进制小数点右边有无限位数且都是1,它们的总和仍小于2。因此,尾数可以表示的值一定介于1.0和2.0(不包括2.0)之间。
我们来看一些例子。以十进制值1.7997为例,我们可以按照以下步骤计算它的二进制尾数:
1.1.7997减去2 0 得到0.7997和
%1.00000000000000000000000
2.0.7997减去2 -1 (1/2)得到0.2997和
%1.10000000000000000000000
3.0.2997减去2 -2 (1/4)得到0.0497和
%1.11000000000000000000000
4.0.0497减去2 -5 (1/32)得到0.0185和
%1.11001000000000000000000
5.0.0185减去2 -6 (1/64)得到0.00284和
%1.11001100000000000000000。
6.0.00284减去2 -9 (1/512)得到0.000871和
%1.11001100100000000000000
7.0.000871减去2 -10 (1/1,024)(近似)得到0和
%1.11001100110000000000000
尽管1和2之间有无限多个值,但是单精度格式的尾数只有23位(第24位始终为1),因此单精度格式能表示的值就只有800万(2 23 )个,而精度也只有23位。
尾数格式采用的是1的补码,而不是2的补码。这意味着24位尾数的值只是一个无符号二进制数,值为正还是负由第31位的符号位确定。1的补码有一个少见的特性,即0有两种表示形式(符号位为1或0)。通常只有浮点软件或硬件系统设计师才会关注这一点。这里我们假设数值0的符号位始终为0。
单精度浮点格式如图4-2所示。
图4-2 单精度(32位)浮点格式
将尾数乘以阶码指定的2的次幂,得到的值有可能超出尾数自身可以表示的范围。单精度浮点格式的阶码有8位,并且采用了增码-127(Excess-127)格式(有时称为移码-127格式阶码,Bias-127Exponent)。在增码-127格式中,值127($7f)表示阶码2 0 。阶码本来的值加上127就转换成了增码-127格式。例如,1.0的单精度表示形式为$3f800000。尾数为1.0(包括隐含为1的高位在内),阶码为2 0 ,编码为127($7f)。2.0的表示形式为$40000000,阶码2 0 的编码为128($80)。
增码-127格式的阶码,使两个浮点数大小的比较得到简化。阶码的比较和无符号整数比较一样,那么就只剩下符号位(第31位)需要单独处理了。如果两个值的符号不同,则其中的正值(第31位为0)大于(最高位为1)负值。 如果两个值的符号位都为0,则直接把两个值当作无符号二进制数进行比较。如果两个值的符号位都为1,同样可以当作无符号数比较,但是结果需要反过来(小于变成大于,大于变成小于)。有些CPU的32位无符号数比较要比32位浮点数的比较快得多,所以在浮点数比较中使用整数运算代替浮点运算就很有必要了。
单精度浮点数的24位二进制尾数的精度大约相当于6位半的十进制数的精度(这个半位精度的含义是,前6位数字的值可以在0..9的范围内,而最后的第7位数字只能在0~ x 的范围内,这里 x <9,通常约为5)。采用8位增码-127格式阶码,单精度浮点数的动态范围约为2 ±128 或者10 ±38 。
尽管单精度浮点数适合许多应用程序,但它的动态范围并不能满足许多金融、科学及其他一些应用程序的需要。而且,在连续的长串计算中,单精度浮点数有限的精度可能会导致明显的误差。重要计算需要的浮点格式精度应该更高。
双精度浮点格式可以解决单精度浮点格式的问题。双精度格式的长度是单精度的两倍,由11位的增码-1,023格式阶码、53位的尾数(包括隐含为1的高位)及一位符号位组成。动态范围大约是10 ±308 ,而精度达到了15~16+位十进制数字。这对于大多数应用程序来说已经足够了。双精度浮点格式如图4-3所示。
图4-3 双精度(64位)浮点格式
为了保持长串双精度浮点数计算的精度,英特尔公司还设计了扩展精度格式。扩展精度格式一共使用了80位,包括64位的尾数、15位的增码-16,383格式阶码,还有一位符号位。尾数的高位并不始终隐含为1。扩展精度浮点格式如图4-4所示。
图4-4 扩展精度(80位)浮点格式
80x86 FPU的所有计算都采用扩展精度形式。FPU在加载单精度或双精度浮点数时,会自动将它们转换为扩展精度。同样,当单精度或双精度浮点数被存储到内存时,FPU会在存储前自动将它们舍入到合适的大小。扩展精度格式为32位和64位计算提供了大量保护位,有助于保持(但不能保证)计算过程中32位或64位精度的完整性。某些进入低位的误差是无法避免的,因为FPU无法为80位计算提供保护位(在80位计算中,FPU只有64个尾数位可用)。虽说80位计算不是绝对精确的,但是,扩展精度格式的准确性通常比双精度的64位更高。
支持浮点运算的非英特尔CPU通常仅支持32位和64位浮点格式。因此,这些CPU的浮点计算得到的结果和采用80位的80x86比起来可能没那么精确。还有一点需要注意,现代的x86-64CPU的SSE扩展中包括额外的浮点硬件。但是,这些SSE扩展只支持64位和32位浮点计算。
当初80位的扩展精度浮点格式只是权宜之计。如果“类型应该保持一致”,那么比64位浮点格式更长的应该是128位浮点格式。可惜在1970年代后期设计浮点格式的时候,英特尔发现四精度(128位)浮点格式的硬件实现成本太高,不得已采用了80位扩展精度格式这样的折中方案。现如今一些CPU(比如IBM的POWER9及高版本的ARM)都能够进行四精度浮点运算。
IEEE Std 754四精度浮点格式采用一位符号位、15位增码-16,383格式阶码以及112位(第113位隐含为1)尾数(如图4-5所示)。精度可以达到36位十进制数字,阶码范围大约为10 ±4932 。
图4-5 四精度(128位)浮点格式