人们使用计算机处理信息。无论被处理信息的实质形态如何千差万别,计算机内部只能处理离散化的编码后的数据。目前,计算机系统内部的所有数据均采用二进制编码。
常见的数有整数和实数之分,整数的小数点固定在数的最右边,通常省略不写,而实数的小数点则不是固定的。但是,计算机中只能表示0和1,无法表示小数点,因此计算机中表示数值数据必须要解决小数点的表示问题。我们通过约定小数点的位置来解决该问题。小数点位置约定在固定位置的数称为定点数,小数点位置约定为可以浮动的数称为浮点数。
1 定点数的表示
在明确了进位制和小数点位置的约定之后,整数在计算机中的表示还有一个正负号如何表示的问题要解决。针对这一问题,原码和补码这两种编码规则采用了不同的解决思路。
1.1 原码
数的原码表示采用“符号-数值”的表示方式,即一个形如的原码表示,最高位是符号位,0表示正数,1表示负数;其余位表示数值的绝对值。如果是0,则A表示正数;如果是1,则A表示负数。例如,对于 19和-19这两个数,如果用8位二进制原码表示,则 19的原码是,-19的原码是。
原码表示有两大优点:
1)与人们日常记录正负数的习惯接近,与真实数值之间的对应关系直观,利于与真实数值相互转换。
2)原码实现乘除运算比较简便直接。
但是原码表示亦存在两个缺点:
1)存在两个0,即一个 0,一个-0。这不仅有悖于人们的习惯,也给使用带来不便。
2)原码的加减运算规则复杂,这对于逻辑实现的影响很大。在进行原码加减运算时,需要首先判断是否为异号相加或同号相减的情况,如果是的话则必须先根据两个数的绝对值的大小关系来决定结果的正负号,再用绝对值大的数减去绝对值小的数。
权衡上述利弊,现代计算机中基本不使用原码来表示整数。原码仅在表示浮点数的尾数部分时采用。
1.2 补码
补码是定点数的另一种表示方法。现代计算机中基本都是采用补码来表示整数。它最大的好处就是可以用加法来完成减法运算,实现加减运算的统一。这恰好解决了原码表示所存在的最大问题。
在补码表示中,其最高位同原码一样也作为符号位,0表示正数,1表示负数。补码表示和原码表示的差异在于其数值的计算方法。对于一个形如的补码表示,其值等于。如果是0,则补码和原码一样,A表示正的;如果是1,则A表示减去(共n-1个0)得到的数。(A未考虑符号位)
这里介绍一个原码和补码之间的转换方法:最高位为0时,原码与补码相同;最高位是1时,原码的最高位不变,其余位按位取反后末位加1。举个例子,譬如 19这个数,如果用8位二进制原码表示是00010011,最高位是0,所以其二进制补码也是00010011。那么对于-19这个数,其原码就是把 19原码的最高位从0变为1,即10010011。在求-19的补码时,原码最高位的1保持不变,原码余下的7位0010011按位取反得到1101100,末位再加一个1,得到1101101,最终得到-19的8位补码是11101101,这个值实际上是由 19的8位补码减去10000000得到的。(00010011-10000000 = - (10000000-00010011)=-1101101)
求一个数的补码是个取模运算。
n 进制基数就是 n模的余数就是 0, 1, …, n-1
“取模”实质上是计量器产生“溢出”的量,它的值在计量器上表示不出来,计量器上只能表示出模的余数(取模);任何有模的计量器,均可化为加减法运算。
“模”是指一个计量系统的计数范围,如时钟,12个整点为计算范围,则模为12。
在12模的时钟中,假设当前时针指向10点,而准确时间是6点,调整时间可有以下两种拨法:
① 倒拨4小时,即:
10-4=6
(10-4) mod 12 = 6
② 顺拨8小时:
10 8=12 6=6
(10 8) mod 12 = 6
在以12模的系统中,加8和减4效果是一样的;因此凡是减4运算,都可以用加8来代替。对“模”而言,8和4互为补数。实际上以12模的系统中11和1、10和2、9和3、7和5、6和6都有这个特性;共同的特点是两者相加等于模。
一个int类型的整数,在32位系统中,用4个字节,32个位来计量,其模为2^32。在模范围内能表达的有 [0, 2³²-1]。在C语言中,也有不考虑符号的整数类型unsigned int,将全部的32位用来表示数,而没有符号位一说。相当于就是整数的绝对值,因为现实世界中某些数的模拟,是没有负数的。
如果要表示负数,也就是C的signed int,则必须有符号位,也就是上述的的补码方案。正数 负数正好达到模的溢出阀值2³²,这也是在计算机中负数是用补码方式表达的原因。
下面用一段代码来表示上述的一些关系:
#include <stdio.h>#include <cmath>void complement(){ int a = 19; // 0 0000000 00000000 00000000 00010011 int b = -19; // 1 1111111 11111111 11111111 11101101 unsigned int c = 0xffffffff-19 1; // 0xffffffff 1是int的模,unsigned不使用符号位 int d = a c; // 模溢出 //unsigned int e = pow(2,31)-19 pow(2,31);// pow(2,31)-15是非符号位取反 1, // 后面的 pow(2,31)是符号位取反 // pow(2,31) pow(2,31) = pow(2,32),int的模 unsigned int e = (1<<31)-19 (1<<31); // (1<<31)是pow(2,32)的简便写法 if(b == c) // b会提升为unsigned,二进制串按unsigned编码解释 printf("a补 = 模数(z^(sizeof(a)*8))-an"); else printf("not the same value!"); if(d == 0) printf("原码加其补码后其值溢出后为0n"); printf("%d,%dn",c,e);}int main(){ complement(); while(1); return 0;}/* output:a补 = 模数(z^(sizeof(a)*8))-a原码加其补码后其值溢出后为0-19,-19*/
利用补码基于模运算的这个特点,可以把减法转换成加法来做,因此在计算机中不用把加法器和减法器分开,只要有加法器就可以做减法。
2 浮点数的表示
计算机中用于数据存储、传输和运算的部件的位数都是有限的,所以采用定点数表示数值数据时有一个不足之处,就是表示范围有限,太大或太小的数都不能表示。同时定点数表示精度也有限,用定点做除法不精确。此外,定点数也无法表示数学中的实数。所以,计算机还定义了浮点数,用来表示实数并弥补定点数的不足。
2.1 二进制的科学记数法
在具体介绍计算机浮点数表示规格前,我们先回忆一下日常书写实数时所采用的科学记数法。譬如可以记为,可以记为。一个采用科学记数法表示的数,如果尾数没有前导零且小数点左边只有一位整数,则可称为规格化数。既然我们可以用科学记数法来表示十进制实数,也可以用科学记数法来表示二进制实数。其一般的表示形式为:
其中s表示符号,f为尾数域的值,e为指数域的值。
譬如二进制实数的科学记数法表示:
2.2 IEEE 754浮点数标准
计算机中的浮点数表示沿用了科学记数法的表示方式,即包含了符号、尾数和阶码三个域。符号用一位二进制码表示,0为正,1为负。然而在计算机内部位宽是有限的,余下的尾数和阶码两者间存在一个此消彼长的关系,需要设计者在两者间权衡:增加尾数的位宽会提高表示的精度但是会减少表示的范围,而增加阶码的位宽虽然扩大了表示的范围但是会降低表示的精度。因为浮点数规格的定义融入了设计者自身的考虑,所以直到20世纪80年代初,浮点数表示格式还没有统一标准,不同厂商的计算机内部的浮点数表示格式存在差异。这导致在不同厂商计算机之间进行含有浮点数的数据传送或程序移植时,必须进行数据格式的转换,更为糟糕的是,有时这种数据格式转换会带来运算结果不一致的问题。因此,从20世纪70年代后期开始,IEEE成立委员会着手制定统一的浮点数标准,最终在1985年完成了浮点数标准IEEE 754的制定。该标准的主要起草者是美国加州大学伯克利分校数学系教授William Kahan,他帮助Intel公司设计了8087浮点协处理器,并以此为基础形成了IEEE 754标准,他本人也因此获得了1987年的图灵奖。自IEEE 754标准颁布后,目前几乎所有的计算机都遵循该标准来表示浮点数。在过去的几十年间,IEEE 754标准也根据工业界在CPU研发过程中遇到的新需求、实现的新结构,及时进行演进和完善。其中一个比较重要的版本是2008年更新的IEEE 754-2008。该版本中明确了有关融合乘加(Fused Multiply-Add)运算、半精度浮点数等方面的内容。本书仅介绍IEEE 754标准中涉及单精度、双精度浮点数表示的基本内容,对其他内容感兴趣的读者可查阅相关文献。
2.3 IEEE 754标准浮点数格式
IEEE 754标准中定义了两种基本的浮点数格式:32位的单精度格式和64位的双精度格式:
32位单精度格式中包含1位符号、8位阶码和23位尾数;64位双精度格式中包含1位符号、11位阶码和52位尾数。两种格式下基数均隐含为2。
IEEE 754标准中,尾数用原码表示。由于表示同一个数的时候尾数可以有多种表示,例如可以表示为,也可以表示成,因此需要一个规格化的表示来使得表示唯一。IEEE 754标准中规格化尾数的表示统一为1.xxxx的形式。尾数规格化后第一位总为1,因而可以在尾数中缺省这一位1。隐藏该位后尾数可以多一位表示,精度提高一位。
IEEE 754标准中,阶码是用加偏置常量的移码表示,但是所用的偏置常量并不是通常n位移码所用的,而是,因此,单精度和双精度浮点数的偏置常量分别为127和1023。
我们可以用代码来模拟一下double的64位双精度格式:
#include <stdio.h>void floatNumber_1(){ struct FF{ // 小端模式模拟double类型编码 unsigned l:32; // 剩下的小数位 unsigned m:15; // 剩下的小数位 unsigned k:5; // 取5位小数 unsigned j:11; // 阶码 unsigned i:1; // 符号位 }; union UN { double dd; FF ff; }; UN un; un.dd = -15.75; // -1111.11 printf("%dn",un.ff.i); // 1 printf("%dn",un.ff.j); // 1023 3 printf("%dn",un.ff.k); // 31 也就是二进制的11111}void floatNumber_2(){ struct FF{ // 小端模式模拟double类型编码 unsigned l:32; // 剩下的小数位 unsigned m:15; // 剩下的小数位 unsigned k:5; // 取5位小数 unsigned j:11; // 阶码 unsigned i:1; // 符号位 }; union UN { double dd; FF ff; }; UN un; un.ff.i = 1; un.ff.j = 1023 3; un.ff.k = 31; // 二进制的11111 un.ff.m = 0; un.ff.l = 0; printf("%.2lfn",un.dd); //un.dd = -15.75;// -1111.11}int main(){ floatNumber_1(); floatNumber_2(); while(1); return 0;}/*1102631-15.75*/
IEEE 754标准对浮点数的一些情况做了特殊的规定,总的来说可以分为5种情况,主要用阶码进行区分,下表给出了IEEE 754标准中单精度和双精度不同浮点数的表示。
(1)无穷大:阶码全1尾数全0
引入无穷大是为了在出现浮点计算异常时保证程序能够继续执行下去,同时也为程序提供一种检测错误的途径。 ∞在数值上大于所有有限浮点数,−∞在数值上小于所有有限浮点数。无穷大不仅可以是运算的结果,也可以作为运算的源操作数。当无穷大作为源操作数时,根据IEEE 754标准规定,可以得到无穷大或非数的结果。
(2)非数(NaN):阶码全1尾数非0
非数(NaN)表示一个没有定义的数。引入非数的目的是检测非初始化值的使用,而且在计算出现异常时程序能够继续执行下去。非数根据尾数的内容又可以分为发信号非数(Signaling NaN)和不发信号非数(Quiet NaN)两种。如果源操作数是Quiet NaN,则运算结果还是Quiet NaN;如果源操作数是Signaling NaN,则会触发浮点异常。
(3)规格化非0数:阶码非全0非全1
阶码e的值落在[1, 254](单精度)和[1, 2046](双精度)范围内且尾数f是非0值的浮点数是规格化的非0数。其尾数经过规格化处理,最高位的1被省略,因此如果符号位是0,则表示数值为(单精度)和(双精度);如果符号位是1,则表示数值为(单精度)和(双精度)。
(4)非规格化非0数:阶码全0尾数非0
在规格化非0数中,能表示的浮点数的最小阶值是-126(单精度)和-1022(双精度),如果浮点数的绝对值小于(单精度)和(双精度),该如何表示呢?IEEE 754允许特别小的非规格化数,此时阶码为0,尾数的小数点前面的那个1就不再添加了。因此如果符号位是0,则表示数值为(单精度)和(双精度);如果符号位是1,则表示数值为(单精度)和(双精度)。非规格化数填补了最小的规格化数和0之间的一段空隙,使得浮点数值可表示的精度进一步提升了很多。
(5)零:阶码全0尾数全0
根据符号位的取值,分为 0和-0。
ref:
https://foxsen.github.io/archbase/运算器设计.html
-End-