浮点数的存储原理(畅谈计算机整数)

人类的世界里,数字通常是十进制表示的(例如0,1,2,3,4,1000等),计算机的世界只能识别二进制,因此对于它说,数字就是二进制(0001,0010等)。

这篇文章重点阐述二进制整数,二进制浮点数在计算机里存储和运算。

二进制整数的存储和运算

一个二进制整数可以采用位置计数法表示,以一个N位的整数为例如下图

整数位置计数法

二进制整数可以分为【无符号二进制整数】和【有符号二进制整数】

1.1【无符号二进制整数】的存储和加减

【无符号二进制整数】是非负数的整数

值范围:

一个n位的无符号整数,它的数值范围如下:

无符号整数的范围

例如8位的无符号整数的范围为0-255,16位的无符号整数的范围为0-65535。

转化为十进制的方式:

以一个n位的无符号整数为例,采用如下图的方式转化为十进制数

无符号整数转化十进制

以一个4位的无符号整数如[0001],[0101],[1011],[1111]为例,转化为十进制如下图

4位无符号整数转化十进制值例子

【无符号二进制整数】的加减:

在阐述【无符号二进制整数】的加减法之前,先说明一下每个二进制位的运算规则,每个二进制位的运算规则与十进制类似,只不过二进制的基数是2,运算规则如下:

二进制位的运算规则

通过上图得知,加法运算会进行进位,减法运算会进行借位。

二进制整数的加减法分为两种:无符号整数加减法和有符号整数加减法。

【无符号二进制整数】的加法运算

从最低位开始即从右向左,每一位都参与运算,对于当前每一位运算过程中产生的进位参与到下一位(当前位左边)的运算中,下图为一个8位二进制数的加法运算例子,红色部分为进位,如下图所示:

无符号二进制整数的加法运算例子

上面的4个加法例子,都没有发生上溢(超出了最大值255),但两个【无符号二进制整数】相加是会发生上溢的,上溢后计算机硬件选择将溢出的位去掉,将剩下的所有位作为结果,如下图

8位无符号二进制整数上溢

上图为11111111 00000001即255 1=256,256超过了8位无符号整数的最大值255,会将溢出的位(红色部分的1)去掉,剩下的00000000作为运算结果即0

【无符号二进制整数】的减法运算

从最低位开始即从右向左每一位都参与运算,对于0-1的差为1,要从左侧借位,下图为一个8位二进制数的减法运算例子,红色部分为借位,如下图所示:

无符号二进制整数的减法运算例子

上面的4个减法例子,都没有发生下溢(小于最小值0即负数),但两个【无符号二进制整数】相减是会发生下溢的,下溢后计算机硬件选择将溢出的位去掉,将剩下的所有位作为结果,如下图

无符号二进制整数的下溢运算例子

【无符号二进制整数】下溢发生在一个小的数减去大的数,如上图01100011=99,10110000=176,99-176=-77,采用传统的运算,小数减去大数规则就是大数-小数即10110000-01100011,然后对结果加上符号,计算机不能这么算,因为这里是无符号运算,不能有负数,因此很多计算机硬件选择直接用01001101即77作为结果,忽略掉负号。

对于常见的程序语言c,c 是支持【无符号二进制整数】的,java没有【无符号二进制整数】,只支持【有符号二进制整数】,另外【无符号二进制整数】编码是唯一性的,不存在同一个【无符号二进制整数】对应不同的二进制编码。

1.2 【有符号二进制整数】的运算

【有符号二进制整数】的编码规则有3种即原码,反码,补码,在几乎所有的计算机硬件加减运算中采用的是补码,先上一个图来区分一下【无符号二进制整数】,原玛,反码,补码的区别,如下面的表格采用4位二进制。

二进制

无符号

原码

反码

补码

0000

0

0

0

0

0001

1

1

1

1

0010

2

2

2

2

0011

3

3

3

3

0100

4

4

4

4

0101

5

5

5

5

0110

6

6

6

6

0111

7

7

7

7

1000

8

-0

-7

-8

1001

9

-1

-6

-7

1010

10

-2

-5

-6

1011

11

-3

-4

-5

1100

12

-4

-3

-4

1101

13

-5

-2

-3

1110

14

-6

-1

-2

1111

15

-7

-0

-1

如上面的表格所示,同一个二进制采用不同的编码方式,表示的十进制数是不一样,另外,原码,反码,补码都可以表示【有符号二进制整数】,它们的最高位都是符号位,1表示负数,0表示正数,正数的编码都相同,负数的编码各个不同,下面来看看原码,反码,补码的定义。

原码:

最高位为符号位,1表示负数,0表示整数,剩余的位数按照【无符号二进制整数】转化为十进制,例如4位二进制数1001,最高位为1,剩余的位数为001按照【无符号二进制整数】转化为十进制就是1,所以结果为-1,原码的值的计算公式如下图

原码值转化为十进制

以0001,0101,1011,1111为例子,按照上面公式计算如下图所示

原码值转化为十进制例子

反码:

最高位为符号位,1表示负数,0表示整数,对于负数,剩余的位数取反后,再按照【无符号二进制整数】转化为十进制,例如4位二进制数1001,最高位为1,剩余的位数为001,取反后就是110,110按照【无符号二进制整数】转化为十进制就是6,所以结果为-6,反码的值的计算公式如下图

反码值转化为十进制公式

以0001,0101,1011,1111为例子,按照上面公式计算如下图所示

反码值转化为十进制例子

补码:

最高位为符号位,1表示负数,0表示整数,对于负数,剩余的位数取反后,然后加1,再按照【无符号二进制整数】转化为十进制,例如4位二进制数1001,最高位为1,剩余的位数为001,取反后就是110,然后加1=111,111按照【无符号二进制整数】转化为十进制就是7,就是7,所以结果为-7,补码的值的计算公式如下图

补码转化为十进制公式

以0001,0101,1011,1111为例子,按照上面公式计算如下图所示

补码值转化为十进制例子

总结下原码,反码,补码

这三种编码的正数编码都相同,负数编码各不同。

原码在计算机硬件用处不多,在【二进制浮点数】的计算中用到了。

反码与补码比较类似,对于负数的反码和补码,同样的二进制,转化后反码后比转化后的补码大1,从它们的计算公式也可以看出。

快速获取一个n位二进制的补码,有两种:

1.对n位二进制取反后 1就是这个n为二进制的补码,例如0111=7取反后1000 1=1001就是-7,对1001=7取反后0110 1=0111就是7

2.2的n次方-n为二进制数,例如0111的补码就是10000-0111=1001,1001的补码就是10000-1001=0111

补码还有以下几个特点:

1.补码是互补的,x (-x)=0例如一个4位二进制,1001 0111=10000舍弃1就是0000

2.补码0是唯一的,不像原码和反码有两个0,不存在二义性

3.n为二进制补码数范围为-2的n-1次方~2的n-1次方-1,例如一个8位二进制数范围为-128~127,正数部分比负数少了一个数,因为0占用了,因此总共有128个负数,0,127个正数。

4.补码加法和减法使用同样的硬件完成,减法可以用被减数的补码实现,例如3-5,就是用3加5的补码实现。

基于上述的几个特点,在几乎所有的计算机中采用补码来表示【有符号二进制整数】

补码的加减法与【无符号二进制整数】加减法的类似,唯一不同的是出现溢出的处理方式不同,下面介绍一下,如下图

上溢和下溢例子

正如上图发生在两个正数或者两个负数相加时,超过了最大值就是上溢,小于了最小值就是下溢,发生溢出的一个特点就是变成了相反符号位的数,例如上溢变成负数,下溢变成了正数。

正如上图所示如果A和B两个数相加,a表示A的符号位,b表示B的符号位,s表示A和B相加后的结果的符号位。

以情形1举例A=00101,B=00111,结果为01100,a=0,b=0,s=0,代入公式后:

0*0*(not 0) (not 0 *not 0) *0=0 (1*1)*0=0因此没有溢出。

以情形2举例A=01100,B=01101,结果为00111,a=0,b=0,s=1,代入公式后:

0*0*(not 1) (not 0*not 0) * 1=0 (1*1)*1=1,因此溢出了

以情形3举例A=10100,B=10011,结果为11001,a=1,b=1,s=0,代入公式后:

1*1*(not 0) (not 1*not 1) * 0=1*1 0=1,因此溢出了。

1.3 【无符号二进制整数】和补码的转化

我们知道同一个二进制序列,【无符号二进制整数】和补码表示的十进制可能不同,可以通过一个公式在两者进行转化

对于一个n位二进制序列,x为补码(x在n位补码最小值和最大值范围内),补码转化无符号数公式如下图

补码转化为无符号二进制数

举例如n=16,x=-12345,它对应的【无符号二进制整数】就是:-12345 2的16次方即65536=-12345 65536=53191

对于一个n位二进制序列,u为无符号数(u在n位无符号数最小值和最大值范围内),无符号数转化补码公式如下图

无符号二进制整数转化为补码

举例如n=16,u=53191,由于u>2的(16-1)次方-1即32767,因此补码=53191-65536=-12345

【二进制浮点数】的存储和运算

浮点位置计数法与整数位置计数法类似,如下图所示

浮点数位置计数法

【二进制浮点数】转化为十进制:

例如10110.11转化为十进制

10110.11计算过程

【二进制浮点数】很多时候都是一个近似值,只是无限趋近于真实值,例如十进制0.2就无法用【二进制浮点数】精确表示,下面的表格可以说明问题

二进制数浮点数

十进制

0.0

0/2

0.0

0.01

1/4

0.25

0.010

2/8

0.25

0.0011

3/16

0.1875

0.001101

13/64

0.203125

0.00110011

51/256

0.19921875

从上边表格得知,【二进制浮点数】根本没有0.2这个值,而是有一些跟这个值比较接近的值如0.199921875和0.203125

【二进制浮点数】存储:

【二进制浮点数】采用IEEE 754进行存储,这个标准的目的就是规范化浮点数在各类计算机的存储方式,提高程序在不同机器的移植性,目前常用的有单精度浮点格式和双进度浮点格式,如下图

标准单进度格式

标准双进度格式

由上图得知,单进度格式与双进度格式一样,差别在于精度和范围不同,下面介绍上图中的各个位置的含义。

s表示符号区,占1位,1表示负数,0表示整数

e表示偏置指数,它与实际指数不同,实际指数是计算浮点数值时候实际用的指数,偏置指数是存储的时候的指数,后续会详细介绍。

f表示小数部分,浮点数小数点后边的数字。例如1.001这个二进制数,001就是小数部分。

以标准单进度格式为例,来说明标准浮点数的分类,分为四类,如下图所示

标准单进度浮点数分类

从上图得知,偏置指数占8位,是【无符号二进制整数】,范围为0-255,小数部分占23位,根据偏置指数(e)和小数部分(f)的值的不同,标准单精度浮点数分为四类即规范化的,非规范化的,无穷大,NaN。

规范化:

偏置指数在1-254,偏置指数=实际指数 127(127为偏置值即2的7次方-1),实际指数=偏置指数-127,因此实际指数的范围为1-127~254-127=-126~127

小数点部分有效位数为23 1,1是隐藏的浮点数整数部分的1,规范化的浮点数整数部分一直为1,因此不再存储,例如1.000001,红色部分的1不再存储,在从内存获取浮点数后,自动在小数点前边加上1。

非规范:

偏置指数为0即全0,实际指数=1-127=-126,之所以用1-127,是为了非规划的浮点数可以平滑地过渡到规范化的浮点数,这点后续会介绍。

规范化的数,要求浮点数整数部分必须为1,因此不能表示0,非规范数,浮点数整数部分为0,因此可以表示0。

0存在正0和负0两个数,一般情况没什么影响,另外非规划数(对于那些小于最小规范化数)还可以表示无限趋近于0的数,另外越趋近于0,数字越密集。

无穷大:

偏置指数为255即全1,小数点部分全0,当符号位为0时,表示正无穷大,符号位为1时,表示负无穷大,例如两个非常大的数相乘或者除以0,都可以造成无穷大。

NaN:

偏置指数为255即全1,小数点部分不为0,这种情况表示非数字,例如一些计算结果不是浮点数或无穷等,也有些场景下,一个未初始化的数据可以表示为NaN

好了,说了这么多,举个规范化的二进制浮点数转化十进制和十进制转化为浮点数的例子,来理解一下上面的概念。

十进制转化为IEEE 754 浮点数:

以4100.125为例,将它转化为IEEE 754标准的32位精度二进制浮点数

a.将4100.125转化为二进制浮点数,整数部分4100=1000000000100,小数部分0.125转化为0.001,因此4100.125=1000000000100.001。

b.1000000000100.001进行规范化,右移12位为1.000000000100001 * 2的12次方

c.由于为正数,s=0

d.实际指数=12,偏置指数=12 127=139=10001011

f.小数部分00000000010000100000000(1.000000000100001去掉整数部分1,自动扩展到23位,红色部分为扩展的8位)

g.因此结果为01000101100000000010000100000000

十进制转化后浮点二进制

IEEE 754 二进制浮点数转化为十进制

以C46C0000为例,这是个十六进制,转化为二进制格式为11000100011011000000000000000000,如下图所示

二进制浮点数内存图

a.s=1

b.偏置指数=10001000

c.小数部分11011000000000000000000

d.实际指数=偏置指数-127=134-127=7

f.小数部分为11011000000000000000000加上隐藏的整数部分1即1.11011000000000000000000

g.由于s=1,这个数=-1.11011000000000000000000*2的7次方,右移动7位=11101100.0000000000000000=-236

例子结束,谈下一个话题

定义任意精度

上面所阐述的是针对标准单进度格式例子,其实可以定义任意精度的,如下图所示

任意精度

上图可以定义任意进度,小数点部分占k位,偏置指数占n位,浮点数总共占位k n 1,下面为实际指数,偏置指数,偏置数的定义如下

规范化浮点数范围

非规范化浮点数范围

我们可以假设一个k和n比较小一点的数,来看看这个配置下浮点数的范围和趋势,由此可以看出标准化浮点数的细节。

定义偏置指数占位4位(n=4),小数点占位3位(k=3),e为偏置指数,偏置值=7,E为实际指数,定义M为标准浮点数整数部分,定义f为标准浮点数小数部分,2的E次方=H

下面表格定义了非负值实例(k=3,n=4)

描述

二进制

指数

e E H

小数

f M

H*(M f) V 十进制

0

最小的非规格化数

M=0

最大的非规格化数

最小的规格化数

M=1

最大的规格化数

无穷大

0 0000 000

0 0000 001

0 0000 010

0 0000 011

…………….

0 0000 111

0 0001 000

0 0001 001

………………

0 0110 110

0 0110 111

0 0111 000

0 0111 001

0 0111 010

……………..

0 1110 110

0 1110 111

0 1111 000

0 -6 1/64

0 -6 1/64

0 -6 1/64

0 -6 1/64

0 -6 1/64

1 -6 1/64

1 -6 1/64

6 -1 1/2

6 -1 1/2

7 0 1

7 0 1

7 0 1

14 7 128

14 7 128

—————

0/8 0/8

1/8 0/8

2/8 0/8

3/8 0/8

7/8 0/8

0/8 8/8

1/8 8/8

6/8 8/8

7/8 8/8

0/8 8/8

1/8 8/8

2/8 8/8

6/8 8/8

7/8 8/8

—————

0/512 0 0.0

1/512 1/512 0.001953

2/512 1/256 0.003906

3/512 3/512 0.005859

7/512 7/512 0.013672

8/512 1/64 0.015625

9/512 9/512 0.017578

14/16 7/8 0.875

15/16 15/16 0.9375

8/8 1 1.0

9/8 9/8 1.125

10/8 5/4 1.25

1792/8 224 224.0

1920/8 240 240.0

————— 无穷大 ————-

从上面的表格得知,非规范化的最大值7/512,规范化的最小值是8/512,从7/512可以平滑地过渡到8/512,这也就是为什么非规范化数的实际指数=1-偏置置的原因。

到此为止,二进制浮点数的存储讨论结束,下面开始看看二进制浮点数的加减运算,浮点数不能直接运算,需要按照如下流程

a.如果二进制浮点数没有规范化,先进行规范化

b.找出指数较小的那个数,将指数进行对齐,指数差多少,就向右移多少

c.尾数相加(或相减)

d.结果进行规范化

e.检查规划化的数是不是 出现上溢或下溢。

如下图为流程图

举个例子如下图所示

浮点数相加例子

番外篇 二进制整数的扩展

什么是整数扩展,例如short扩展为int,int扩展为long

【无符号二进制整数】扩展整体右移,高位补0

【补码】扩展,整体右移,高位补符号位

例如16位【无符号二进制整数】53191内存为:CFC7,扩展为32位【无符号二进制整数】后内存为:0000CFC7

例如16位【补码】-12345内存为:CFC7,扩展为32位【补码】后内存为:FFFFCFC7

二进制整数的截断

什么是整数截断,例如int截断为short,long截断为int

【无符号二进制整数】截断

假设截断前的值x,截断后的值为x',截断后的值的位数为k,那么截断后的值的公式为

无符号二进制整数截断公式

unsigned int ux = 65537;#00000000 00000001 00000000 00000001
unsigned short us = (unsigned short)ux;#高位截断后00000000 00000001

例如上面代码例子k=16,截断后的值就是65537%2的16次方=65537e536=1

【补码】截断

假设截断前的值x,截断后的值为x',截断后的值的位数为k,那么截断后的值的公式为

先采用【无符号二进制整数】截断

无符号二进制整数截断公式

然后对【无符号二进制整数】截断的结果采用如下图的方式转化为补码

无符号二进制整数转化为补码

例如

int ux = 53191;
short us = (short)ux;

例如上面代码例子k=16

先采用【无符号二进制整数】截断,截断后的值就是53191%2的16次方=53191e536=53191

在将【无符号二进制整数】转化为补码即53191-65536=-12345

二进制整数的移位

二进制整数的乘法和除法占用的CPU周期较长,乘法的10几个周期,除法的30几个周期,因此有时候为了提高效率,可以通过移位来解决。

二进制整数的移位包括左移,逻辑右移,算术右移

左移不区分逻辑或算术,因此它都是右边补0,是相等。

右移分为逻辑右移和算术右移,逻辑右移统一左侧补0,算术右移统一左侧补符号位,如下面表格

操作

参数x

[01100011] [10010101]

x<<4

[00110000] [01010000]

x>>4(逻辑右移)

[00000110] [00001001]

x>>4(算术右移)

[00000110] [11111001]

,

本文Hash:84fa23890e43fec2daf6af3eee59d4eeccdd4814

声明:此文由 leaf 分享发布,并不意味 赞同其观点。文章内容仅供参考,此文如侵犯到您的合法权益,请联系我们。

Baidu
map