起飞就起飞

golang浮点数精度问题

Posted on By baixiao

记录一个最近遇到的浮点数精度问题。

示例

已知商品价格为1元(数据库记录为100分),有优惠条件售价降低为原价80%,求现在买一件商品优惠多少钱?

代码:

func math1() {
	fmt.Println("math1")
	var count int64 = 1
	var price int64 = 100
	var rate int64 = 80

	a := float64(rate) / 100.0
	fmt.Println("a:", a)
	b := 1 - a
	fmt.Println("b:", b)
	c := float64(count*price) * b
	fmt.Println("c:", c)
	discount := int64(c)
	fmt.Println("discount:", discount)
}

输出:

math1
a: 0.8
b: 0.19999999999999996
c: 19.999999999999996
discount: 19

最后优惠19分。这么简单都要出错!

原理

重新学习了一下浮点数的原理,文章《What Every Programmer Should Know About Floating-Point Arithmetic》讲得不错。归纳有几点:

why

计算机无法精确存储大部分浮点数

和整数一样,计算机用二进制来存储浮点数。但是计算机无法精确存储大部分浮点数,举个例子,在代码中的0.1,经过编译或者解释(interpreted)之后就已经被一个接近真实值的二进制数字来代替了,也就是说还没开始运算数值就已经变得不精确了。

用python来演示也是一样:

>>> a=80/100.0
>>> a
0.8
>>> 1-a
0.19999999999999996
>>> b=50/100.0
>>> b
0.5
>>> 1-b
0.5

为何有些浮点数又能精确处理,比如0.5

0.5即1/2,只要分母是2的幂次方就能精确处理,这个后续会详解。

what,具体是怎么造成的

二进制表示

大家都很清楚整数的二进制表示如下:

相对应的小数的二进制表示如下:

那么问题来了,对于无理数(Pi)或者是小数位很多的有理数而言,计算机不可能有足够多的或者无限多的bit位来保存它,那么不可避免的就有精度问题。相对的,如果能用有限个分母是2的幂次方的小数组成的话,就能精确表示。

浮点数的真正含义

实际上浮点数用科学计数法来表示,运用尾数和指数,外加符号来表示。IEEE754定义的单精度和双精度浮点数如下:

需要注意符号位在首,且指数有127位(单精度)或者1023位(双精度)的偏移量来表示正负。举一个例子:比如十进制数123.125,其二进制表示为:1111011.001,规格化表示为:1.111011001×pow(2,6) 也就是1.111011001×pow(2,133−127),f(尾数)= 111011001,E(指数) = 133 = 10000101,图示如下:

how,如何避免精度问题

按照自身的需求对小数点后多少位进行截断

代码:

func math2() {
	fmt.Println("math2")
	var count int64 = 1
	var price int64 = 100
	var rate int64 = 80

	a := Round(float64(rate)/100.0, 5)
	fmt.Println("a:", a)
	b := Round(1-a, 5)
	fmt.Println("b:", b)
	c := Round(float64(count*price)*b, 5)
	fmt.Println("c:", c)
	discount := int64(c)
	fmt.Println("discount:", discount)
}

func Round(f float64, n int) float64 {
	n10 := math.Pow10(n)
	return math.Trunc((f+0.5/n10)*n10) / n10
}

输出:

math2
a: 0.8
b: 0.2
c: 20
discount: 20	

使用精准类型(Exact Types)

使用”github.com/shopspring/decimal”包,将对浮点数进行精确计算。 代码:

func math3() {
	fmt.Println("math3")
	var count int64 = 1
	var price int64 = 100
	var rate int64 = 80

	f1 := decimal.NewFromFloat(float64(rate))
	f2 := decimal.NewFromFloat(100)
	a := f1.Div(f2)
	fmt.Println("a:", a)
	b := decimal.NewFromFloat(1).Sub(a)
	fmt.Println("b:", b)
	f3 := decimal.NewFromFloat(float64(count))
	f4 := decimal.NewFromFloat(float64(price))
	c := f3.Mul(f4).Mul(b)
	fmt.Println("c:", c)
	discount := c.IntPart()
	fmt.Println("discount:", discount)
}

输出:

math3
a: 0.8
b: 0.2
c: 20
discount: 20

这个包实际上用到了https://golang.org/pkg/math/big/,其作为Arbitrary-Precision Decimal的一种,主要原理就是加大尾数和指数的位数,但是降低了性能。