Float的基本介绍
Posted by 付辉 on Wednesday, January 16, 2019 共2560字关于浮点数,为什么它生来就可能存在误差?带着好奇查阅了一些介绍,然后做了简单汇总。这只是一段知识的开始,后续还会继续完善。
—— 荡荡上帝,下民之辟。疾威上帝,其命多辟。天生烝民,其命匪谌。靡不有初,鲜克有终。
Floating-point represent
浮点数在计算机中是如何表示的?因为它有小数点,那么小数点后面的数,该如何用二进制来表示呢?我们都知道,浮点数本身就存在误差,在工作中,你所使用的float
都是一个近似数。这又是什么原因导致的呢?
1. Fixed-point
fixed-point
是将bit
位拆分成固定的两部分:小数点前的部分和小数点后的部分。拿32 bit
的fixed-point
表示举例,可以将其中的24 bit
用于表示整数部分,剩余的8 bit
表示小数部分。
假如要表示1.625
,我们可以将小数点后面的第一个bit
表示$\frac12$
,第二个bit
表示1/4
,第三个1/8
一直到最后一个1/256
。最后的表示就是00000000 00000000 00000001 10100000
。这样其实也好理解,因为小数点前是从$2^0$开始向左成倍递增,小数点后从$2^{-1}$
开始向右递减。
因为小数点后面的部分始终小于1,上面这种表达方式能表达的最大数是255/256
。再比这个数小,这种结构就无法表示了。
Floating-point basics
根据上面的思路,我们用二进制表达一下5.5
这个十进制数,转化后是$101.1_{(2)}$
。继续转换成二进制科学计数法的形式:$1.011_{(2)} * 2^2$
。在转换的二进制科学计数法过程中,我们将小数点向左移了2位。就跟转换十进制的效果一样:$101.1_{(10)}$
的科学计数形式为$1.011 * 10^2$
。
对于二进制科学计数法表达的5.5
,我们将其拆分成2部分,1.011
是一部分,我们称为mantissa
。指数2是另一部分,称为exponent
。下面我们要将$1.011_{(2)} * 2^2$
映射到计算机存储的8 bit
结构上。
我们用第一个bit
来表示正负符号,1表示负数,0表示正数。紧接着的4 bit
用来表示exponent + 7
后的值。 4 bit
最大可以表示到15
,这也就意味着当前的exponent
不能超过8
,不能低于-7
。最后的3 bit
用于存储mantissa
的小数部分。你可能有疑问,它的整数部分怎么办呢?这里我们约定整数部分都调整成1,这样就可以节省1 bit
了。举个例子,如果要表示的十进制数是0.5,那么最后的二进制数不是$0.1_{(2)}$
,而是$1.0 * 2^{-1}$
。最后表示的结果就是:0 1001 011
。
再来一个decode
的例子,即将0 0101 100
还原回原始值。根据之前的描述0101
表示的十进制是5,所以exponent = -2
,表示回二进制科学计数法的结果:$1.100 * 2^{-2} = 0.011_{(2)}$
。我们继续转换成真实精度的数:0.375
。
最后可以看在,如果mantissa
的长度超过3 bit
表示的范围,那么数据的存储就会丢失精度,结果就是一个近似值了。
1. Representable numbers
继续按照上面的思路,现在8 bit
的浮点表示能表示的数值区间更大。
要表示最小正数的话,sign
置为0,接下来的4 bits
置为0000
,最后的mantissa
也置为000
。那么最终的表示结果就是:$1.000_{(2)} * 2^{-7} = 2^{-7} ≈ 0.0079_{(10)}$
。
表示最大正数的话,sign
置为0,其他位也都置为1。最终表示的结果:$1.111_{(2)} * 2^{8} = 111100000_{(2)} = 480_{(10)}$
。所以8 bits
浮点表示的正数范围(0.0079, 480]
。而8 bits
二进制表示的范围是[1, 127]
。范围确实大了很多。
但是必须注意:浮点数无法准确表示该区间内的所有数。拿十进制51来说,用二进制表示是110011
。转化为8 bits
的浮点数表示:$110011_{(2)} = 1.10011_{(2)}*2^{5}$
。当我们试着去存储的时候,发现3 bits
的mantissa
放不下现在的10011
。我们不得不做近似取值,将结果修改为$1.101_{(2)} * 2^{5} = 110100_{(2)} = 52_{(10)}$
。所以,在我们8 bits
表达的浮点数中51 = 52
。这样的处理有时候让我们很无奈,但这也是为了让8 bits
表示更大范围的数所必须付出的代价。
从上面的过程中,我们还可以理解在计算中round up
和round down
的策略。当小数点后的数超过3 bit
时,就是展现这个策略的时候。拿19
举例,表示成二进制科学计数法:$1.0011 * 2^4$
。如果执行round up
,最终的结果就是$1.010_{(2)} * 2^4 = 20_{(10)}$
。如果执行round down
,结果便是$1.001_{(2)} * 2^4 = 18$
。
如果我们要提高浮点数表达的精度,mantissa
区间就需要更多的bit
来表示。拿float32
来举例,它是1 bit
的sign
,8 bits
的exponent
以及23 bits
表示的mantissa
。
IEEE standard
该标准定义了更长的bit
来提高表达的精度和范围。
1. IEEE formats
它定义了上面描述的sign
、exponent
、mantissa
以及excess
(就是8 bits
表示过程中用到的7)。
sign | exponent | mantissa | exponent | significant | |
---|---|---|---|---|---|
format | bit | bits | bits | excess | digits |
Our 8-bit | 1 | 4 | 3 | 7 | 1 |
Our 16-bit | 1 | 6 | 9 | 31 | 3 |
IEEE 32-bit | 1 | 8 | 23 | 127 | 6 |
IEEE 64-bit | 1 | 11 | 52 | 1,023 | 15 |
IEEE 128-bit | 1 | 15 | 112 | 16,383 | 34 |
2. 非数值
顾名思义:not a number
,程序中偶尔会看到的NaN
。比如0/0
、∞ + −∞
等。这类数值在表示中exponent
都是1。
3. 运算
讨论 x + (y + z)
和 (x + y) +z
的结果是否相同,拿上面8 bits
的浮点数表示来说明。其中x=1
,y=52
,z= -52
。我们注意到y+z = 0
,所以第一个计算结果是1。但(x+y)
的结果仍然是52
,这主要是因为mantissa
无法表示,导致最终结果取近似值还是52,最终结果是0。
另外一个例子:1/6 + 1/6 + 1/6 + 1/6 + 1/6 + 1/6 = 1
等式也是不存在的。在8 bits
的表示中无法准确的表示1/6
,所以最终的结果要比1小。
在程序开发过程中,我们必须意识到这类问题产生的影响。
float to float
Round
返回最近的整数,但返回值是一个float64
类型。返回值是四舍五入后的结果。
a := math.Round(12.3456)
//output: 12
相对应的函数,还有Floor
和Ceil
// Floor returns the greatest integer value less than or equal to x.
// output: 12
a := math.Floor(12.3456)
// Ceil returns the least integer value greater than or equal to x.
// output: 13
a := math.Ceil(12.3456)
match/big
关于浮点数的比较:
// change these value and play around
float1 := 123.4568
float2 := 123.45678
// convert float to type math/big.Float
var bigFloat1 = big.NewFloat(float1)
var bigFloat2 = big.NewFloat(float2)
// compare bigFloat1 to bigFloat2
result := bigFloat1.Cmp(bigFloat2)
参考文章: