更新于 

定点数的表示与运算

数据的存储方式和排列

数据的大端/小端存储

我们知道对于一个int型的数是4B,而存储方式又是按字节存储,对于一般的数据都是采用连续存储,所以我们可以知道一个4B的内容数据存储时是需要占据连续4个地址空间单元,我们假设现在有一个int型变量i,他的机器数为01 23 45 67H,那么一般我们不用最左或者最右来形容这个数的两边,这与大端/小端的存储方式有关,我们一般形容为最低有效字节LSB和最高有效字节MSB,所以01是MSB,67是LSB,注意H是16进制的意思。那么我们现在讲解一下大端存储和小端存储。

假设我们现在要将上面的数存储到一段连续的4个存储空间,分别为0800H,0801H,0802H,0803H,那么01 23 45 67H分别放到哪个空间中呢?按照大端存储(Big endian)方式存那么就是我们通常理解的从高有效字节到低有效字节的顺序存储:

0800H 0801H 0802H 0803H
01H 23H 45H 67H

这样我们到真是够指针访问时就是直接得到了01234567H,如果是小端存储(Little endian)方式,那么是按照最低有效字节到最高有效字节存储:

0800H 0801H 0802H 0803H
67H 45H 23H 01H

这样我们顺序访问存储单元后得到的是67452301H,所以需要转换一下每次都将后面数放到前面即可。这样才能读出正确的数值01234567H。

那么在实际的底层汇编语言,我们要根据不同的数据存储类型得到正确的读取方式才能够读出正确的数据,比如对于由反汇编器(汇编逆过程,将机器代码转换为汇编代码)得到的文本表示:

1
4004d3  01 05 64 94 04 08    add %eax, 0x8049464

上面一个常见的汇编文本,前面4004d3是执行到的指令地址,01 05 64 94 04 08分别是指令的机器代码,add是一种相加的汇编指令,%eax是一个寄存器用来存储数据,一般变量的数据会从内存中拿出到寄存器中,而0x8049464就是一个立即数。假设现在是小端存储,那么执行这条指令时,从指令代码的后4个字节取出这个立即数,立即数存放的字节序列就应该是64 94 04 08H(最后一个8要补0)。

所以在阅读小端存储方式的机器代码时,要时刻注意字节是按相反形式显示的。

“边界对齐”方式存储

我们首先来介绍一下按字节和字,半字寻址方式的关系:

寻址方式 定义 所寻位数
按字节寻址 一次搜索跳过一个字节宽度 8bit
按字寻址 一般是一次搜索跳过整数个字节,具体多少字节根据机器类型不同,但是一定是整数个字节 8bit*n
按半字寻址 顾名思义,一次跳过半个字 4bit*n

假设有一个机器存储字长为32位,可以按照字节,半字和字寻址,那么如果数据是以边界对齐的方式存储,半字地址一定是2字节的整数倍,字地址一定是4字节的整数倍,这样无论所取的数据时字节,半字还是字,都可以一次访存取出。当所存储的数据不满足上面的要求时,就将填空字节使其满足边界对齐,这样虽然浪费了存储空间,但是加快了搜索:下图的任意一个数据都必定一次访存得到。

如果不是边界对齐的方式存储,那么虽然可以充分利用存储空间,但是半字或者字长的指令可能会存储在两个存储字中,此时我们就需要两次访存了,从而影响了指令的执行效率:

此时我们尝试取出字1或者半字3的数据内容都需要两次访存,这主要是因为数据内容分开存储到了两个字中。

边界对齐方式相对边界不对齐方式是一种空间换时间的思想,RISC如ARM采用边界对齐方式,而CISC如x86对齐和不对齐都支持。因为对齐方式取指令时间是确定相同的(就是一次访存的时间),因此更能适应指令流水。

浮点数的表示

浮点数的表示格式

浮点数,和定点数不同,他是以适当的形式将比例因子表示在数据中,让小数点的位置根据需要浮动,这样既扩大了表示范围,同时也保证了数的有效精度。通常浮点数的表示形式为:

N=rEMN=r^E*M

为什么浮点数的表示形式是这样的呢?我们可以拿科学计数法表示小数做对比,比如电子的质量:

91028=911028{r=10(r就是10进制基底10E=28M=99*10^{-28}=9^1*10^{-28}\begin{cases} r=10(r就是10进制基底10)\\ E=-28\\ M=9 \end{cases}

所以我们发现上面的基数10是隐藏的,且M(9)都不能超过基数10,因为是10进制,那么我们类比写出浮点数的形式:

其中Jr和Sr分别代表的是阶码和尾数的正负,所以Jm和Sr之间是小数点的位置,他会随着m和n阶码和尾数的长度变化而浮动位置。其中阶码就是类比于刚刚的级数-28,但是这里由于是机器语言,r一般是2即二进制,所以J1J2…JM是一个二进制串,而后面的尾数类比于9,但是同样是以二进制形式表示,但是E和M肯定都是定点数。这样我们只要给出任意一个阶码E和尾数M我们都可以求出这个浮点数的真值。并且由于有正负问题,一般我们都用补码来表示阶码和尾数:

对于一个浮点数a他的阶码和尾数分别如下:

E=0.01M=1.1001E=0.01 \quad M=1.1001

那么阶码是一个正数很容易就可以转成10进制真值为+1,而尾数是一个补码,我们需要先转换原码-0.0111再求出10进制真值:

{方法一:直接按照定点小数求解0.0111=(22+23+24)=7/16方法二:可以看成定点整数111.=7右移4位变成.111所以除以16=7/16\begin{cases} 方法一:直接按照定点小数求解-0.0111=-(2^{-2}+2^{-3}+2^{-4})=-7/16\\ 方法二:可以看成定点整数-111.=-7右移4位变成-.111所以除以16=-7/16 \end{cases}

这样我们就算出了E=+1,M=-7/16,所以再带入浮点数计算公式就可以求解:

a=21(7/16)=7/8a=2^{1}*(-7/16)=-7/8

现在我们要用1B来存储,那么也就是8位:

7(Jf) 6(J1) 5(J2) 4(Sf) 3(S1) 2(S2) 1(S3) 0(S4)
0 0 1 1 1 0 0 1

刚好可以将阶码和尾数拼接放在一起,且此时小数点是在5和4之间。

对于一个浮点数b他的阶码和尾数分别如下:

E=0.01M=0.01001E=0.01 \quad M=0.01001

阶码还是一个正数转换成真值就是+1,而尾数也很好计算:

{方法一:直接按照定点小数求解+0.01001=+(22+25)=9/32方法二:可以看成定点整数+1001.=9右移5位变成+.01001所以除以32=+9/32\begin{cases} 方法一:直接按照定点小数求解+0.01001=+(2^{-2}+2^{-5})=9/32\\ 方法二:可以看成定点整数+1001.=9右移5位变成+.01001所以除以32=+9/32 \end{cases}

这样我们就算出E=+1,M=+9/32,所以再带入浮点数计算公式就可以求解:

b=21(9/32)=9/16b=2^1*(9/32)=9/16

现在我们也用1B来存储,但是发现貌似有点问题:

001001001貌似是9位,1B放不下(因为尾数最后一位是1不能丢弃,如果丢弃了就是近8/16近似9/16了,不是准确的9/16了)。所以此时我们尝试对这个浮点数码进行转换:

b=21(+0.01001)=20(+0.01001+0.01001)=20(+0.10010)b=2^1*(+0.01001)=2^0*(+0.01001+0.01001)=2^0*(+0.10010)

所以阶码转换成了+0,位数M转换成了+0.10010,这样再拼接,由于最低位是0可以丢弃不影响真值,所以可以用1B存储:

7(Jf) 6(J1) 5(J2) 4(Sf) 3(S1) 2(S2) 1(S3) 0(S4)
0 0 0 0 1 0 0 1

小数点仍然是在5和4之间,我们发现尾数能占用的位数就是总位数减去阶码占用的位数,而小数点就在阶码和尾数的交界处。我们思考一下b在存储时还需要手动进行对阶码和尾数的转换以便能够存储,那么我们能不能有一种规则保证了尾数和阶码可以恰好占满存储位数而不会出现丢位缺失精度的方法?答案是有的,请看下面:

规格化浮点数

为了能够提高运算的精度,我们需要充分利用尾数的有效位数,所以我们要规格化浮点数,实际上规格化就是保证最高位数位必须有一个有效值,非规格化浮点数需要进行规格化操作才可以变成规格化浮点数。

我们在规格化的时候,一般是要进行以下两个操作的:

  • 左规:当浮点数运算的结果为非规格化时要进行规格化处理,将尾数算术左移一位(注意是算术左移),阶码减1(基数为2时)的方法称为左规,左规可能会进行多次。
  • 右规:当浮点数运算的结果尾数出现溢出(双符号位01或者10)时,将尾数算术右移一位,阶码加1(基数为2时)的方法称为右规。需要右规时,只需进行一次即可。

那么规格化浮点数后应该满足尾数M的绝对值:

1/r<=M<=11/r<=|M|<=1

若是r=2,即基数为2,用的是2进制,那么1/2<=|M|<=1

  • 原码规格化以后

    正数为0.1×××…×的形式,其最大值就表示为0.11…1,最小值就是0.100…0。

    尾数范围是1/2<=M<=(12n)尾数范围是1/2<=M<=(1-2^{-n})

    负数为1.1×××…×的形式,其最大值表示为1.10…0,最小值为1.11…1。

    尾数范围是(12n)<=M<=1/2尾数范围是-(1-2^{-n})<=M<=-1/2

  • 补码规格化以后

    正数为0.1××…×的形式,其最大值表示为0.11…1,最小值就是0.100…0。

    尾数范围是1/2<=M<=(12n)尾数范围是1/2<=M<=(1-2^{-n})

    负数为1.0××…×的形式,其最大值表示为1.01…1,最小值表示为1.00…0。

    尾数范围是1<=M<=(1/2+2n)尾数范围是-1<=M<=-(1/2+2^{-n})

思考:为什么补码的负数最值边界表示发生了变化?

首先我们需要遵循一个规则才是规格化浮点数,就是上面所讲的原码形式的尾数最高位必须是1,补码的尾数最高位必须和符号位相反。那么在这个前提下我们观察原码的最值,正数0.1开头,就已经保证了M一定是>=1/2了,同时要求最大值,那么后面就全部填1尽可能的多加2^n最终话会无限逼近1,所以最大值时0.11…1,而最小值同理后面全补0。而负数最大值就是尽可能的不要在加2^(-n)了,所以全部补0,得到最大值1.10…0,最小值就是尽可能的全加上2^(-n),所以为1.11…1。那么在得到原码的基础上我们来观察补码,补码正数的部分和原码相同,但是负数部分,我们认为就是将原码的负数最大值转换成补码应该就得到的是补码最大负数值。可是我们看原码的最大负数值1.10…0的补码应该是1.1…0虽然确实满足了|M|>=1/2,但是却不是负数规格化的形式(要求必须是尾数最高位和符号位相反)。所以这不符合,我们取比1.10…0小一点的次大值1.10…01,他的补码是1.01…1满足|M|>=1/2并且还满足了负规格化浮点数的补码要求,所以他才是最大负值。至于最小值我们认为应该也是将原码的负数最小值转换成补码应该就得到的是补码最小负数,所以应该是1.00…01,但是实际上不是这个值,而是1.00…00,这是为什么呢?我们可以这样理解原码的最小负数值1.11…1假设尾数部分有n个1,那么总会有一个更小的值即1.11…11尾数部分有n+1个1,其补码就会是1.00…001(多了个n+1位是1),取前n位就得到了1.00…00。所以我们可以理解为补码1.00…0是负无穷。

思考:为什么规格化浮点数后原码形式尾数最高位必须是1?为什么补码形式的尾数最高位和符号位要相反?

实际上我们思考一下刚刚的b,他的未转换前的尾数是9/32<16/32=1/2。那么此时他的尾数最高位就不是1而是0,也就是说如果不满足尾数>1/2,那么尾数的第一个1之前就会可能有许多个不确定的0,比如9/32的尾数是.01001前面有1个0,再比如3/64的尾数是.000011前面有4个0,这样我们就不能给出一个准确的位数来存储尾数,但是如果我们保证了尾数一定>=1/2。那么他的第一个位必须是1,这样才能保证至少为1/2,也就是说此时我们保证了尾数的第一个1的前面不会有许多个不能确定的0了,即有效位。这样我们就可以将尾数保证在一定的位数范围内存储了,当然这是对于原码来说就是最高位必须为1了,对于补码正数来说也是最高位为1且此时补码的符号位为0,所以就必定是0.1开头,而对于补码负数来说,其产生是原码取反加1所以就必定是最高位为0了,又因为此时符号位一定是1,所以就是1.0开头了。所以我们可以总结出一个规律,对于二进制的规格化浮点数,尾数的原码最高位必须是1,尾数的补码最高位必须和符号位相反。

思考:如果不是二进制,而是4进制,8进制规格化浮点数又是什么规律?

我们思考前面提到了对于r进制需要保证|M|>=1/r,所以对于2进制才有了|M|>=1/2,这样也就推理出来尾数的最高位必须是1了,那么如果对于4进制,那么|M|>=1/4,也就是说尾数的前两位只能是01,10,11这三种情况才能保证尾数>=1/4,所以只用要求尾数原码的前两位不是全0即可,相对应的对于8进制,原码规格形式的尾数最高3位不能全为0即可,而此时补码形式的规律就不太好推了,我们也无需掌握。

浮点数的溢出

对于定点数我们定义过上溢和下溢,这里浮点数同样也有溢出的概念,我们可以将浮点数的溢出按照下面这张图进行定义:

  1. 一个浮点数是否溢出一定要看规格化以后的浮点数
  2. 运算结果大于最大正数时称为正上溢,小于绝对值最大负数(就是最小负数)时称为负上溢,正上溢和负上溢统称为上溢。
  3. 运算结果在0~最小正数时称为正下溢,在0~绝对值最小负数(就是最大负数)时称为负下溢,正下溢和负下溢统称为下溢。
  4. 只有在数据上溢时,计算机才必须中断运算操作,进行溢出处理(原因是此时存储位置不够导致丢失了有效位造成了数值精度下降,可能会影响后面的操作),但是对于数据下溢,浮点数会趋于0,计算机只是将其当做机器0处理。

思考:定点数和浮点数的溢出概念区别?

溢出类型\数值类型 定点数 浮点数
上溢 大于最大正值 正上溢:大于最大正值
负上溢:小于最小负值
下溢 小于最小负值 正下溢:0~最小正值
负下溢:0~最大负值

定点数只要溢出就中断,但是浮点数只有上溢会中断。

浮点数的IEEE754标准

我们已经了解了浮点数的表示和规格化的方法了,但是这样还是不易于存储表示,我们看此时对于一个规格化的浮点数的解码和尾数都有原码,补码,等不同的表述形式,并且都有正负的问题,所以接下来我们学习一个重要的浮点数表示标准,即IEEE754标准。说来也简单,我们看下图:

我们规定,这个浮点数的正负单独存储在最前面的数符位来表示正负,而阶码必须是由移码表示,且尾数必须使用原码表示。我们根据不同的浮点数二进制码的长度分成了三种情况,代表了不同的精度,存储时只能是这三种的任意一种。

段浮点数就是我们熟知的float类型,他是短浮点数,一共需要32位来存储浮点数,而长浮点数double精度更高,是64位,还有一个是不太常见的临时浮点数又称为扩展双精度浮点数long double为80位。我们观察一下上表,思考几个问题:

思考:偏置量是什么?

我们在学习移码的时候讲过偏置量,就是对于一个n+1位的移码,其有以下规律:

[X]=(偏置量)2n+X[X]_移=(偏置量)2^n+X

所以偏置量是用来将阶码的真值转换成移码的,又因为阶码有固定的的位数,所以三种浮点数都对应这固定的偏置量。

思考:为什么偏置量变成了2^n-1?为什么IEEE754中没有阶符?

我们原先学习移码的偏置量时规定偏置量为2^n,但是这里却减小了1,我们首先需要透彻理解一下为什么要引出偏置量,我们知道一个级数的指数是有正负之分的,比如2^9和2^(-9),但是这样我们还需要拿出一个位单独来表示正负(即阶符),这样我们对于解读这个阶码就很复杂,所以我们引出了移码,那么移码的作用就很明显了,为了能够全部按照无符号数(即非负数)解读也可以表示负指数。那么我们怎么做到呢?就需要引入一个偏置量,我们可以将原先8位阶码按正负解读的-127~127更改为全部按照无符号数解读的0~255,但为了得到-127~127我们需要减去一个数,这个数可以是127或者128,这样我们就可以将E总是按照无符号数解读最后减去一个偏置量即可表示出-127~127了。所以采用移码以后,阶码E必定是解读为一个无符号数,只要在减去一个偏置量即可表示出负指数了。但是我们又规定在IEEE754中11111111阶码不能解读为+255,而是需要特殊表示为无穷大,而00000000也不能解读为0,而是非规格化浮点数,所以E实际上只能解读的无符号数范围是1~254,那么对应着指数的范围就变成了-126~-0以及1~127,即-126~127,那么对应着IEEE754的偏置量为127时即可恰好保证阶码E解读为[1,254]减去一个偏置量127表示为指数的[-126,127]。所以在IEEE754中偏置量时2^n-1,而在非IEEE754的情况下偏置量就是2^n。

思考:为什么11111111和00000000在IEEE754中要单独拿出来特殊表示?

我们知道11111111应该原先表示为无符号数的127,但是我们原先在溢出哪里讲过无穷可以使用全1来表示,所以11111111在IEEE754中失去了127的数值意义,反而用来表示了无穷。同样的,00000000也具有特殊意义表示非规格化浮点数,至于为什么,规定而已。

然后我们在来继续讲解IEEE754浮点数的表示,经过上面的思考,我们知道数符是来标识浮点数的正负的,而阶码绝对是一个无符号数,经过减去偏置量即可完美表示出级数的指数正负,那么也就不存在阶符了,这也是阶码必须使用移码的原因。但是对于尾数我们还必须采用原码,原因是这样我们可以用23位来表示纯小数数值的24个有效信息位。实际上就是因为尾数采用了隐藏位策略的原码表示导致的。

思考:隐藏位原码为什么可以使得尾数用23位表示24个数值?

我们思考一下原码的特点:尾数的最高位必定是1。那么我们可不可以隐含这个最高位1,这样不就可以多表示1位了吗?例如对于十进制的12,我们将它改写成二进制串是1100,用浮点数表示应该写成0.1100*2^4,但是最高位必定是1,那么我们就可以隐藏不填写他,即改成1.100*2^3,这样前面的1我们就可以不占用尾数的一位1,相应的,尾数就可以多表示1位了。所以23位可以表示24位尾数值。

思考:二进制1100怎么就快速转换成了0.1100*2^4?

我们知道在十进制的科学计数法中9*10^6可以快速转换成0.9*10^7,实际上就是尾数真值除以基数10,将这个10给了阶码,所以阶码+1。那么1100是同样这样转换的,1100=1100*2^0,那么1100.右移4位即除以基数2四次,变成了0.1100,同样给了阶码4个2就变成了0.1100*2^4。

对于短浮点数和长浮点数都采用了这种尾数隐藏1位的原码表示,所以尾数都可以多表示一位有效数值,但是临时浮点数就没有,他就是正常存储(毕竟位数多不缺这一位)。那么我们就可以写出IEEE754浮点数的真值计算公式了:

{短浮点数:(1)ms1.M2E127长浮点数:(1)ms1.M2E1023\begin{cases}短浮点数:(-1)^{m_s}*1.M*2^{E-127}\\ 长浮点数:(-1)^{m_s}*1.M*2^{E-1023} \end{cases}

M是23位加上前面的1就是24位尾数表示值了,这就是隐含原码多一个表示位的原因。那么同样我们也可以根据浮点数真值转换IEEE754标准的浮点数表示了。上式中,ms是数符0表示正,1表示负,短浮点数E的取值是1~254(8位),M为23位可以表示24位。而长浮点数E的取值是1~2046(11位表示),M为52位可以表示53位。

并且我们可以得到以下结论(以短浮点数为例):

  1. 只有E=0且M=0时,表示真值为0

  2. E=0,M≠0,则为非规格化数,真值为

    (1)ms0.M2126(-1)^{m_s}*0.M*2^{-126}

    一定要注意此时M小数点前面就不是1了,就是正常的0了,因为非规格化数的尾数最高位不能保证是1了,所以此时23位尾数就是表示23个有效数值位了

  3. 1<=E<=254,真值就是短浮点数真值计算公式的计算结果,一定要注意偏置量为127

  4. E=255且M≠0时,真值为‘NaN’(非数值)

  5. E=255且M=0时,真值为正无穷或负无穷(看符号位)

我们可以计算出单精度短浮点数和双精度长浮点数的表示范围了

当然啦此时E=0,M≠0的非规格化浮点数可以表示的更小,但是他不符合IEEE754标准要求,也就不属于单精度和双精度浮点数的表示范围,所以我们还可以推断出IEEE754表示范围内的数理论上一定是一个规格化浮点数。但是实际上IEEE754标准也可以用来表示非规格化数。

定点数、浮点数的区别

角度一:数值的表示范围

如果定点数个浮点数的字长相同,那么浮点表示法所能表示的数值范围将远远大于顶点表示法。

角度二:精度

精度,就是一个数所包含的有效数值位的位数。对于字长相同的定点数和浮点数,浮点数虽然扩大了数的表示范围,但是精度降低了。

这里有点小疑惑,为什么浮点数精度会下降呢?我们可以想一想因为定点小数小数点就在最高位,那么后面的位都是有效位,而对于浮点数,同样的位数,却要分出一部分用来表示阶码,这样虽然扩大了表示范围,但是尾数就短了,相应的精度就低了。比如定点数可以表示0.007783526,但是在浮点数中,可能只能表示成7.78*10^(-3)了。

角度三:数的运算

浮点数包括阶码和尾数两部分,运算时不仅要做尾数的运算,还要对阶码进行运算,而且运算结果还要求规格化,所以浮点数比定点数运算要复杂。

角度四:溢出问题

在定点运算中,当运算结果超出数的表示范围,发生溢出,而在浮点运算中,运算结果超出尾数表示范围却不一定溢出,只有规格化后阶码超出所能表示的范围时,才发生溢出。

为什么浮点数运算结果可能不会溢出呢?因为小数点可以根据阶码和尾数的具体长度浮动,如果尾数很长需要更多的位,而恰巧阶码不需要很多位,那么阶码可以让出一些为给尾数,即小数点浮动了,但是一旦规格化以后根据IEEE754要求,尾数和阶码必须占据固定尾数就可能产生溢出了。

总结

浮点数的运算

加减运算

浮点数运算的特点是阶码运算和尾数运算要分开进行,浮点数的加减运算一律采用补码进行。浮点数的加减运算包括以下几个步骤。

对阶

我们知道在科学计数法中,加减运算时要求级数相同,这里也是。对阶是使两个操作数的阶码相同,也就等价于两个操作的小数点位置相同。这里我们要求小阶向大阶对齐,即阶码小的操作数,尾数右移1位相当于除以2,那么阶码就可以加一。直到两个数的阶码相等为止。尾数右移时,那么会舍掉有效位产生误差,影响精度。

一定要注意,由于最终短阶码要变成长阶码,所以必定会造成占用了尾数的位,所以尾数右移会少位,这就会造成精度降低的原因。

尾数求和

对阶后的尾数按照定点数加减运算规则进行。一定要注意如果操作数是按照IEEE754格式写的,需要先将移码转换成真值或者补码在进行加减运算。

规格化

我们以双符号位为例,当尾数大于0时,即是一个大于时,其补码规则是

[S]=00.1××...×[S]_补=00.1××...×

当尾数小于0时,即是一个负数时,其补码规则是

[S]=11.0××...×[S]_补=11.0××...×

这里的双符号位前面讲过,如果有遗忘,速速回去复习->传送门。可见,当补码形式的尾数,数值最高位与符号位必须相反,只有当尾数的最高数值位和符号位不同时,即为规格化形式,规格化分为左规和右规两种。

  1. 左规:当尾数出现00.0××…×或者11.1××…×时,需要左规,即尾数左移一位,和的阶码减1,直到尾数为00.1××…×或11.0××…×。
  2. 右规,当尾数求和结果溢出(如尾数为10.××…×或者01.××…×)时,需要右规,即尾数右移一位,和的阶码加1。这里我们要注意其实此时并不是真的结果溢出了,他最终经过右规以后是可以表示的,毕竟浮点数的表示范围非常大,一般是不会溢出的。

舍入

我们知道在对阶和右规的过程中(都是尾数右移的操作),会将尾数的低位丢弃,引起误差,影响精度。其中舍入规则有两种:

  • “0”舍“1”入法:类似于十进制的运算中的四舍五入法,即在尾数右移时,被移去的最高数值位为0,那么就直接舍去。如果被移去的最高数值位为1,那么就在尾数的末位加1,这样做可能会使尾数又溢出,此时需要再做一次右规。
  • 恒置“1”法:尾数右移时,不论丢掉的最高数值位是1还是0,都使右移后的尾数末位恒置1,这种方法同样有使尾数变大和变小的可能。

我们距离说明一下,比如对于0.xxxx|010100,|后面的是丢弃数串,那么如果按照“0舍1入法”,因为被丢弃的是010100最高位是0,所以就直接丢弃即可,剩余的尾数为没有变化, 如果被舍弃的是110100,那么剩余的尾数应该为0.xxx+0.xx1即最后一位加1。如果是恒置1法,那么无论被舍弃的串什么样子,最后剩余的尾数最低位必须置为1,即变成0.xx1。上面这两种方法都不能避免尾数精度的降低,毕竟尾数的位数减少了,这是精度降低的根本原因,但是上面两种方法一定程度上缩小了误差。

溢出判断

和定点数加减法一样,浮点数加减运算的最后一步也是需要进行溢出判断的,一定要注意规格化的右规时不时结果溢出,只有规格化以后才进行真正的溢出判断。

我们在规格化讲了尾数的01或者10并不是真正的溢出,只是需要右规即可。其实浮点数的溢出判断,是根据阶码来判断的,假设浮点数的阶码此时是双符号位补码形式,那么如果出现01,即阶码大于最大阶码,那么就是上溢,进入中断处理。当阶码为10,即阶码小于最小阶码,那么就是下溢,此时只是看成机器0(无论尾数是多少都是0),不会发生中断。实际上原理还是阶码符号位不同表示溢出,且真符号位和最高位一致才不溢出(00,11)。

例题

那么接下来我们以一道例题讲解浮点数加减运算的整体过程:

已知十进制数X=-5/256,Y=+59/1024,按照机器补码浮点运算规则计算X-Y,结果用二进制表示,浮点数格式如下:阶符取2位,阶码取3位,数符取2位,尾数取9位。用补码和尾数来表示尾数和阶码。

我们首先看完题干,很容易就可以判断出肯定是双符号位了,并且一定要知道因为阶码是补码形式,所以才给出了阶符,如果阶码是移码,那么就不要阶符了。现在我们进行求解:

首先我们将X转换成规格化的浮点数形式:

5D=101B1/256=28>X=10128=0.10125=0.10121015D=101B,1/256=2^{-8}->X=101*2^{-8}=-0.101*2^{-5}=-0.101*2^{-101}

所以X的二进制浮点数形式为(将尾数和阶码均转换成补码,并且符号位为负数11正数00)

11011,11.011000000{阶码:101原码就是11101>补码:11011尾数:0.101原码就是11.101>补码:11.011>扩展:11.01100000011011,11.011000000\begin{cases} 阶码:-101原码就是11101->补码:11011\\ 尾数:-0.101原码就是11.101->补码:11.011->扩展:11.011000000 \end{cases}

我们再将Y转换成规格化浮点数形式:

59D=111011B1/1024=210>Y=+111011210=+0.11101124=+0.111011210059D=111011B,1/1024=2^{-10}->Y=+111011*2^{-10}=+0.111011*2^{-4}=+0.111011*2^{-100}

所以Y的二进制浮点数形式为(将尾数和阶码均转换成补码,并且符号位为负数11正数00)

11100,00.111011000{阶码:100原码就是11100>补码:11100尾数:+0.111011原码就是00.111011>补码:00.111011>扩展:00.11101100011100,00.111011000\begin{cases} 阶码:-100原码就是11100->补码:11100\\ 尾数:+0.111011原码就是00.111011->补码:00.111011->扩展:00.111011000 \end{cases}

X,Y都写成了规格化浮点数形式以后我们进行对阶:

X阶码减去Y阶码:

ΔE=[X]阶补[Y]阶补=[X]阶补+[Y]阶补=11011+00100=11111>真值:1ΔE=[X]_{阶补}-[Y]_{阶补}=[X]_{阶补}+[-Y]_{阶补}=11011+00100=11111->真值:-1

所以X阶码比Y小1,所以X进行一次尾数右移,阶码加一,右移时算术右移,因为尾数是补码,右移是算术右移,所以高位补1变成:

X11011,11.011000000>11100,11.101100000X:11011,11.011000000->11100,11.101100000

对阶完成以后,我们进行尾数的加减:

[X]尾数[Y]尾数=[X]尾数+[Y]尾数=11.10100000+11.000101000=10.110001000[X]_{尾数}-[Y]_{尾数}=[X]_{尾数}+[-Y]_{尾数}=11.10100000+11.000101000=10.110001000

完成尾数加减运算以后,我们进行规格化

对于现在的结果,尾数是10.110001000,阶码是11100,发现此时X-Y的结果尾数符号位是10,需要右规,算术右移,同时阶码加1:

11101,11.011000100{阶码加一:11100+00001=11101尾数算术右移高位补1:10.110001000>11.01100010011101,11.011000100\begin{cases} 阶码加一:11100+00001=11101\\ 尾数算术右移高位补1:10.110001000->11.011000100 \end{cases}

这样我们一次右规就完成了规格化

接下来我们进行舍入,我们知道舍入一般是因为尾数或者阶码长度过程需要舍弃才会涉及舍入,但是题干给出的位数很多,我们不但不需要舍弃低位,还要扩展位数,所以依旧没有舍入的问题。

最终我们再判断结果是否溢出,查看阶码符号位是11,没有溢出,所以最终结果就是

11101,11.011000100>真值:23(0.1001111)211101,11.011000100->真值:-2^{-3}*-(0.1001111)_2

经过验证答案正确,那么我们现在思考几个问题:

思考:为什么右归和对阶时都没有涉及舍入?

我们看一下题干发现给的阶码位数和尾数位数是固定的,所以对阶时只是阶码表示的真值不同,但是两个操作数X和Y所占用的阶码位数确实必定一样的,所以就不涉及到某一个操作数阶码短需要增长占用尾数位数的问题,所以自然尾数也就不会有丢位,也就不涉及 到舍入。而右规时本来有可能涉及到丢位的,即尾数太长了比规定的位数还要长则可能需要丢位,但是现在尾数给了9位,足够放下所有尾数有效位,所以也不涉及到了舍入。所以舍入一般情况下不会出现(毕竟很难,题干会有意避开),但是实际上浮点数的运算时经常涉及到舍入问题的。

思考:全过程难点有哪些?

首先就是小数的转换,例如-0.101,我们可以先看成正数0.101,前面加了一个负号变成了负数,所以符号位是1,又因为是双符号位所以转换成了11.0101,还有就是补码加减运算的规则,忘记了一定要去复习,还有补码算术右移规则,以及最后结尾转换成真值时补码要转换成真值或者原码以及结果可能溢出需要判断的细节问题。

强制转换

这里的情况很复杂,我们不进行定量分析,只要能定性分析即可。我们以C语言举例,float和double分别对应着IEEE754的单精度浮点数和双精度浮点数(这也就是说明float和double阶码都是移码,尾数都是原码),而long double对应着扩展双精度浮点数,一般长度和格式随着编译器和处理器类型的不同而有所不同。在C中,我们知道:

所以char->int->long->double和float->double从前到后都是精度和表示范围从小到大,所以转换过程没有损失,但是注意这几种情况:

  1. int->float:不发生溢出,但是int可以保留32位,而float只能保留24位,可能有数据舍入,如果从int转换成double就不会出现舍入问题。
  2. int或者float->double,因为double的有效位更多,因此能保留精确值
  3. double->float:float范围更小,可能产生溢出,此外,由于有效位变少,可能出现舍入问题
  4. float或者double->int:因为Int没有小数部分,所以数据会向0方向被截断(只保留整数部分),影响精度。最典型的就是(int)0.5=0,另外,由于int表示范围小,因此可能发生溢出。

总结