浮点数学习笔记

浮点数学习笔记

前言

最近在工作中遇到汇率换算的问题,需要将用户订单中的的本币换算成美元,用 golang 写了个脚本进行换算,当把汇率变量的类型定义为 float32 的时候,计算结果存在不能接受的误差
查看了一下 golang 的官方文档,发现 golang 实现的浮点型数据是基于 IEEE 754标准,这一标准在存储小数的时候先天就 存在误差,下面会一一介绍。

float 精度问题

先来个小例子,抛出问题

1
2
3
4
5
6
7
8
9
import "fmt"

func main() {
var a float32 = 64.35
var b = 64.35 // 默认类型是 float64

fmt.Printf("this is a float32 %f \n", a)
fmt.Printf("this is a float64 %f \n", b)
}

程序执行结果:

this is a float32 64.349998
this is a float64 64.350000

可以看到, 使用 float32 类型的时候, 小数 64.35 存储的并不精准;不难想象,对 float32 进行逻辑计算的时候肯定会产生误差;那么使用 float64 类型数据的时候,计算结果就一定精准吗?答案显然是否定的,下面给将给出答案

为什么叫 浮点数

为什么叫 浮点数浮点数 这个名词是相对 定点数来说的,从这两个名词中可以看出,这两个概念的差别就在于 , 这里的 指的是小数中的 小数点

大家都知道,计算机都是使用 二进制 的形式来存储和计算数据的,对于小数的处理也是如此;

存储小数的时候,计算机将小数分为 整数小数 两个部分进行处理:

定点数 就是将小数点的位置固定,分别分配固定的位数用于存储 整数小数 部分,
例如,我们用 32bit 存储小数,第31位存储符号,23~30位存储 整数,0~22 位存储小数,如下图,

0 0000 1111 0100 0000 0000 0000 0000 000 15.25
31bit符号位 23~30bit保存整数部分 0~22bit保存小数部分 十进制小数

上面这个例子中 定点数 将小数点固定在 22bit 和 23bit 之间;可以很明显的看出,这种存储方式受到位数的限制,能表示的数字范围很小,上例中小数的范围就是 -255.xxx ~ 255.xxx

(ps:寡人太懒了,不想算[1/2 + 1/4 + … + 1/2-23])
也正是这个原因,计算机放弃了这种方式,采用了 浮点数 的方式。

浮点数 从名称上来解释的就是,小数点的位置是浮动的;简单来说浮点数就是将一个数字用科学计数法表示,先将数字分为 基数指数; 再将基数分为整数部分和小数部分,例如:
“12345 = 1.2345 x 104;当然这里是十进制,而计算机在存储浮点数的时候当然还是二进制

“15.25 = 1111.01 = 1.11101 x 23

让我们用32bit 保存小数:

0 1000 0010 1110 1000 0000 0000 0000 000 15.25
31bit符号位 23~30bit保存指数部分 0~22bit保存小数部分 十进制小数

你能从这个二进制中看出小数点的位置吗?

这里大家要注意: 浮点数不仅仅可以保存小数!整数也是可以的,但是用浮点数表示整数这种行为不鼓励,毕竟浮点数表示数字是不精确的

IEEE 754 存储 浮点数

上面讲浮点数的时候也基本介绍了 IEEE 754 标准

IEEE 754 标准依赖于 科学计数法,将一个数字用二进制科学计数法表示,将一个数字分为指数基数,用一位表示符号位,再将基数分为整数(二进制的整数部分当然是1了)和小数部分;IEEE 754规定了四种表示浮点数值的方式:单精确度(32位)、双精确度(64位)、延伸单精确度(43比特以上,很少使用)与延伸双精确度(79比特以上,通常以80位实现);如下:

表示方式 符号位(Sign,S) 指数部分(Exponent,E) 小数部分(Fraction,F)
单精确度(32位) 1bit 8bit 23bit
双精确度(64位) 1bit 11bit 52bit

一个单精确度(32位)浮点数的表达公式如下:

x=(-1)S×(1.F)×2E-127    e=E-127

Sign: 简写为S,符号位,很简单就是 1:负数 0:正数

Exponent: 简写为E,指数部分,计算公式是:指数+接码偏移量(127)

Fraction: 简写为F,小数部分

到这里大家就很自然的会想到一个问题 阶码偏移量为何用127?

这个问题也困扰我很久,看了维基百科等资料,最后在知乎上找到了一个比较靠谱的答案,引用如下:

主要是为了让表示的范围能够对称起来
这个算一算就清楚了。当阶码E 为全0且尾数M 也为全0时,表示的真值x 为零,结合符号位S 为0或1,有正零和负零之分。当阶码E 为全1且尾数M为全0时,表示的真值x 为无穷大,结合符号位S 为0或1,也有+∞和-∞之分。这样在32位浮点数表示中,要除去E,用全0和全1(255)10表示零和无穷大的特殊情况,指数的偏移值不选128(10000000),而127(01111111)。对于规格化浮点数,阶码E范围是1~254。 分两种情况计算如下: 1)偏移值为127时,绝对值范围大致是:1.210^(-38)~3.410^(+38); 2)如果偏移值取为128时, 绝对值范围大致是:5.910^(-39)~1.710^(+38); 可见偏移值取127时,上下范围基本对称,相对合理点。

作者:yuanyuany
链接:https://www.zhihu.com/question/24784136/answer/144601879

来源:知乎

如果是双精确度(64位)

十进制IEEE 754浮点数 相互转化

下面来个小例子, 我们将 64.35 转化成IEEE 754浮点数

“1. 现将 64.35 用转化成 二进制

先看整数部分 64 = 0100 0000

再来转化小数部分转二进制

0.35x2 = 0.7       0 // 取计算结果整数部分

0.70x2 = 1.4       1 // 取上一计算结果的小数部分乘以2

0.40x2 = 0.8       0

0.80x2 = 1.6       1

0.60x2 = 1.2       1

0.20x2 = 0.4       0

0.40x2 = 0.8       0

0.80x2 = 1.6       1

0.60x2 = 1.2       1

0.40x2 = 0.8       0 // 到这里已经是循环重复了,这里永远不会算出1.0,所以用 IEEE 754 表示的时候是无限循环

小数部分: 0.35 = 01 0110 0110 ...

二进制结果 64.35 = 1000000.0101100110... = 1.0000 0001 0110 0110 0110… x 26

用 32bit IEEE 754 存储 64.35

s = 0

expr = 127 + 6 = 133 = 1000 0101

frag = 0000 0001 0110 0110 0110 011

IEEE 754 32位存储 64.35 的情况如下

64.35 = 0 | 1000 0101 | 0000 0001 0110 0110 0110 011

用 64bit IEEE 754 存储 64.35

s = 0

expr = 1023 + 6 = 1029 = 1000 0000 0101

frag = 0000 0001 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110

IEEE 754 64位存储 64.35 的情况如下

64.35 = 0 | 1000 0000 0101 | 0000 0001 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110

我们再将IEEE 754浮点数 还原成小数,

先看看32bit 的情况
看看,算出指数 1000 0101 - 1111111 = 133 - 127 = 6

然后这个浮点数的二进制表示就是 1.0000 0001 0110 0110 0110 011 x 2<sup>6</sup>

下面就是换算成十进制
“1.0000 0001 0110 0110 0110 011 x 26 = 100 0000.0101 1001 1001 1001 1

先看整数部分 0100 0000 = 64
在看小数部分 0.0101 1001 1001 1001 1 转十进制

“1/22 + 1/24 + 1/25 + 1/28 + 1/29 + 1/212 + 1/213 + 1/216 + 1/217 = 0.349052429199219

结论: 0 | 1000 0101 | 0000 0001 0110 0110 0110 011 = 64.349052429199219

再看 64bit 的情况:

“1.0000 0001 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 x 26 = 100 0000.0101 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 10

0.0101 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 10 =
“1/22 + 1/24 + 1/25 + 1/28 + 1/29 + 1/212 + 1/213 + 1/216 + 1/217 + 1/220 + 1/221 + 1/224 + 1/225 + 1/228 + 1/229 + 1/232 + 1/233 + 1/236 + 1/237 + 1/240 + 1/241 + 1/244 + 1/245 = 0.349999999999994

结论: 0 | 1000 0000 0101 | 0000 0001 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 = 64.349999999999994

从这里也可以看出浮点数都不精确!当对精确有要求的时候尽量避免使用浮点数。