Float的基本介绍

关于浮点数,为什么它生来就可能存在误差?带着好奇查阅了一些介绍,然后做了简单汇总。这只是一段知识的开始,后续还会继续完善。

—— 荡荡上帝,下民之辟。疾威上帝,其命多辟。天生烝民,其命匪谌。靡不有初,鲜克有终。

Floating-point represent

浮点数在计算机中是如何表示的?因为它有小数点,那么小数点后面的数,该如何用二进制来表示呢?我们都知道,浮点数本身就存在误差,在工作中,你所使用的float都是一个近似数。这又是什么原因导致的呢?

1. Fixed-point

fixed-point 是将bit位拆分成固定的两部分:小数点前的部分和小数点后的部分。拿32 bitfixed-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 bitsmantissa放不下现在的10011。我们不得不做近似取值,将结果修改为$1.101_{(2)} * 2^{5} = 110100_{(2)} = 52_{(10)}$。所以,在我们8 bits 表达的浮点数中51 = 52。这样的处理有时候让我们很无奈,但这也是为了让8 bits表示更大范围的数所必须付出的代价。

从上面的过程中,我们还可以理解在计算中round upround 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 bitsign8 bitsexponent以及23 bits表示的mantissa

IEEE standard

该标准定义了更长的bit来提高表达的精度和范围。

1. IEEE formats

它定义了上面描述的signexponentmantissa以及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=1y=52z= -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

相对应的函数,还有FloorCeil

// 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)

参考文章:

  1. Golang : Compare floating-point numbers
  2. Floating-point representation
  3. Floating Point Numbers