关于IEEE754浮点数类型

关于IEEE754浮点数类型

不管是Java或C中的Double浮点数, 还是Java或C中的Float浮点数, 甚至那些弱类型语言中的浮点数, 如Python或Javascript中的浮点数, 都遵循这个标准, 实际上这是计算机底层的存储方式与编程语言无关.

接下来我们将会分析十进制浮点数是如何变成二进制存储在内存中的, 以及为什么浮点数有误差不精确, 后续有时间的话再更新一下自制高精度浮点数类型

简介

在六、七十年代,各家计算机公司的各个型号的计算机,有着千差万别的浮点数表示,却没有一个业界通用的标准。这给数据交换、计算机协同工作造成了极大不便。IEEE的浮点数专业小组于七十年代末期开始酝酿浮点数的标准。在1980年,英特尔公司就推出了单片的8087浮点数协处理器,其浮点数表示法及定义的运算具有足够的合理性、先进性,被IEEE采用作为浮点数的标准,于1985年发布。而在此前,这一标准的内容已在八十年代初期被各计算机公司广泛采用,成了事实上的业界工业标准。加州大学伯克利分校的数值计算与计算机科学教授威廉·卡韩被誉为“浮点数之父”。

自20世纪80年代以来, 许多CPU与浮点运算器都是根据IEEE 754实现的。这个标准定义了表示浮点数的格式的二进制表示形式, 包含符号,指数和尾数(小数部分)如下图所示。Infinity,-Infinity和NaN(不是数字)具有特定的位表示形式。

IEEE754规定了两种浮点数:

类型特性
1. IEEE短实型:32位符号1位,指数8位,尾数23位。也称为单精度。(Float)
2. IEEE Long Real:64位符号1位,指数11位,尾数52位。也称为双精度。(Double)

IEEE 754浮点数的三个域

 IEEE 754浮点数的三个域

一个浮点数 (Value) 的表示其实可以这样表示:

Value = sign × 2^exponent × fraction

也就是说, 浮点数的实际值 , 等于符号位(sign bit)乘以指数偏移值(exponent bias)再乘以分(小)数值(fraction)。

下面会依次介绍这三个部分的概念, 用途.

下文没有特殊说明的话, 所有例子都采用Float单精度浮点数. 它与Double基本一致, 但总体域长度较长不利于理解.

32位单精度浮点数的结构
img-c0tzaZIv-1691753438271

sign(符号位)

符号位: 占据最高位(第31位)这一位, 用于表示这个浮点数是正数还是负数, 为0表示正数, 为1表示负数.

举例: 对于十进制数20.5, 存储在内存中时, 符号位应为0, 因为这是个正数

exponent(指数位)

指数位占据第30位到第23位这8位. 在这里存储的指数实际上是做偏移后的指数(biased exponent)

在这里只需要知道这是用于表示以2位底的指数. 8位二进制可以表示256种状态, IEEE754规定, 指数位用于表示[-127, 128]范围内的指数.

一般来说最高位可以直接表示为符号位, 即0000 0001=1, 1000 0001=-1, 但在这里没有采用这种方式, 而是其他方式, 根据观察: 符号位正相反, 符号位为1时是正数, 也可以理解为人为在256个状态中划定了127作为0

Exponent(指数)DEC(十进制数)BIN(二进制数)
2^-12700000 0000
2^-11260111 1110
2^01270111 1111
2^11281000 0000
2^1282551111 1111

当指数为[-127,-1]时浮点数的value在(-1,1)区间

为了更方便计算以及理解, 可以采用下面的方式进行计算偏移后指数:

浮点型的指数位都有一个固定的偏移量(bias), 用于使 指数 + 这个偏移量 = 一个非负整数.

规定: 在32位单精度类型中, 这个偏移量是127. 在64位双精度类型中, 偏移量是1023. 所以, 这里的偏移量是127

⭐ 即, 如果你运算后得到的指数是 -127, 那么偏移后, 在指数位中就需要表示为: -127 + 127(偏移量) = 0

如果你运算后得到的指数是 -10, 那么偏移后, 在指数位中需要表示为: -10 + 127(偏移量) = 117

看, 有了偏移量, 对于十进制来说, 指数位中始终都是一个非负整数.

fraction(尾数位)

尾数位: 占据剩余的22位到0位这23位. 用于存储尾数.

尾数中存储的本质上是二进制的小数部分

Value = sign × 2^exponent × fraction

尾数的区间为: [0, 8388608]

实例

十进制浮点数以32位单精度浮点数(Float)存在内存中的方式

举个例子: 十进制20.5, 如何以二进制存储到内存

大致分为如下几步:

1. 进行进制转换.
2. 二进制数转换为以2为底的指数形式
3. 对尾数进行处理

进行进制转换

一般的计算器都不支持带小数的二进制转换, 包括win10自带的计算器也不行

image-20230810214523208

可以看到小数点已经被禁用了. 在这里我们首先学习一下手动进行带小数的二进制转换, 稍后用一个专门的网站来验证正确性.

因为整数位和小数位的转换方式略有差异, 所以需要把 20.5 分成整数位和小数位分别转换成二进制, 然后再将他们合并在一起.

整数位20转换成2进制: 直接用20/2然后取余数那一套, 口诀: “除2取余,逆序排列”, 详细过程如下:

算式余数
20/2100
10/250
5/221
2/210
1/201

很容易就得出 20D=10100B , 为方便区分不同进制, 我们规定其中的D为10进制后缀, B为二进制后缀.

小数位转为二进制: 口诀: “乘2取整, 顺序排列”, 详细过程如下:

算式
0.5*21.01

等于0.1B

再来一个: 0.625

算式
0.625*21.251
0.25*20.50
0.5*21.01

等于0.101B

根据上面的计算我们得出20.5D=10100.1B

二进制数转换为以2为底的指数形式

用 二进制数 表示 十进制浮点数 时, 表示为尾数*指数的形式, 并把尾数的小数点放在第一位和第二位之间, 然后保证第一位数非0, 这个处理过程叫做规范化(normalized)

二进制10100.1 = 1.01001 * 2^4

我们再来看看规范化之后的这个数: 1.01001 * 2^4

其中1.01001是尾数, 而4就是偏移前的指数(unbiased exponent), 上文讲过, 32位单精度浮点数的偏移量(bias)为127, 所以这里加上偏移量之后, 得到的偏移后指数(biased exponent)就是 4 + 127 = 131, 131转换为二进制就是1000 0011, 这8位二进制就是32位浮点数中实际存储的数值.

对尾数进行处理

为了更进一步的节省内存, 需要对尾数做一些特殊的处理.

隐藏高位1

经过观察发现, 尾数部分的最高位始终为1. 比如这里的 1. 01001, 这是因为前面说过, 规范化之后, 尾数中的小数点会位于左起第一位和第二位之间. 且第一位是个非0数. 而二进制中, 每一位可取值只有0或1, 如果第一位非0, 则第一位只能为1. 所以在存储尾数时, 可以省略前面的 1和小数点. 只记录尾数中小数点之后的部分, 这样就节约了一位内存. 所以这里只需记录剩余的尾数部分: 01001

所以, 以后再提到尾数, 如无特殊说明, 指的其实是隐藏了整数部分1. 之后, 剩下的小数部分

低位补0

尾数不足23位时需要在低位补零, 补齐23位.

之所以在低位补0, 是因为尾数中存储的本质上是二进制的小数部分, 所以如果想要在不影响原数值的情况下, 填满23位, 就需要在低位补零.

比如, 要把二进制数1.01在不改变原值的情况下填满八位内存, 写出来就应该是: 1.010 0000, 即需要在低位补0

同理, 本例中因为尾数部分存储的实际上是省略了整数部分 1. 之后, 剩余的小数部分, 所以这里补0时也需要在低位补0:

原尾数是: 01001(不到23位)

补零之后是: 0100 1000 0000 0000 000 (补至23位)

最终在内存中的状态

在上面的讨论中, 我们已经得出, 十进制浮点数 20.5 的:

符号位是: 0

偏移后指数位是: 1000 0011

补零后尾数位是: 0100 1000 0000 0000 000

现在, 把这三部分按顺序放在32位浮点数容器中, 就是 0 1000 0011 0100 1000 0000 0000 000

这就在32位浮点数容器中, 以二进制表示了一个十进制数20.5的方式

这里有一个可以验证的IEEE754浮点数内存状态的网站, 我们来验证一下:

img-YJB8rxdy-1691753438272

为什么不能精确表示出小数

看了很多篇文章都是从浮点数在计算机存储时会有一个叫做间隔的东西存在, 要么就是说数学中的数值是连续的, 二进制不能够存储数学中连续的值,

计算机的内存或硬盘容量是有限的, 而1到2之间小数的个数是无限的

绕过来绕过去, 甚至还搬出来了维基百科甚至是IEEE754标准来研究他的精度, 个人认为完全没有必要… 在存储精度与内存空间的高效利用之间, 当时的人们义无反顾的选择了后者, 正所谓鱼和熊掌不可兼得, 这也是为什么要这么设计的最根本的原因.

简单一句话就可以概括, 那就是为了实现对空间的最大化利用, 在一开始就规定了一种十进制转换为二进制的方法以及要以指数形式存储的方法, 采用这种进制转换方法就决定了小数部分不能正确的存储进内存中的必然结果.

例如: 0.1这个十进制数永远不能正确的存储进内存, 虽然可以使用更多的bit来实现更高的精度但也只能无限逼近却永远达不到.

算式结果整数
0.1*20.20
0.2*20.40
0.4*20.80
0.8*21.61
0.6*21.21
0.2*20.40

0.1D = 0.00011(0011)无限循环

这种精度其实对于某些对精度要求不高场景也还是可用的,

精度问题

前面说到这种存储方式会误差, 那么这种误差究竟有多大? 误差范围可以接受?

大体来说, 32位浮点数的精度是7位有效数. 这7位有效数, 指的是 整数 + 小数一共7位, 而不是说始终能精确到小数点后六位…

整数 + 小数<=7位 内的精度可以得到保障

根据我的研究发现, 当Float存储数超过16777215, 此时尾数位全1, 即尾数为8388607时, 此时无法存储小数, 并且>16777215的数会有大约1单位误差, 基本无法使用.

如与16777216紧邻的数是16777218, 两数差值为2, 即间隔为2, 这也说明了32位浮点数无法表示出16777217
img-FDnryaoL-1691753438273

从一个数到另一个数之间的距离我们称为间隔, 理想情况下: 整数之间的间隔应该为1, 小数之间的间隔可以无限小, 但是在IEEE754这种存储方式中的间隔在不同的情况下数值的离散程度也不同, 也就是说ieee754中的间隔不是固定的, 而是越来越大.

这里有一份 wiki帮我们总结好了的间隔数据:

img-64QVdK4K-1691753438274

img-QSlG7e6A-1691753438275

自制精确浮点数类型

在了解到这些前因后果, 以及原理之后, 写出一个高精度的, 甚至说的没有误差的数据类型似乎变得可行了, 以后有时间了在更新吧…

参考资料:

十进制小数转化为二进制小数 | 菜鸟教程 (runoob.com)

IEEE 754 - 维基百科,自由的百科全书 (wikipedia.org)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值