IEEE 754 二进制浮点数不精确
Posted
技术标签:
【中文标题】IEEE 754 二进制浮点数不精确【英文标题】:IEEE 754 binary floating-point numbers imprecise for money 【发布时间】:2019-09-27 04:36:53 【问题描述】:当我将math.Floor
与浮点变量一起使用时遇到问题(向下舍入/截断精度部分)。我怎样才能正确地做到这一点?
package main
import (
"fmt"
"math"
)
func main()
var st float64 = 1980
var salePrice1 = st * 0.1 / 1.1
fmt.Printf("%T:%v\n", salePrice1, salePrice1) // 179.9999
var salePrice2 = math.Floor(st * 0.1 / 1.1)
fmt.Printf("%T:%v\n", salePrice2, salePrice2) // 179
游乐场:https://play.golang.org/p/49TjJwwEdEJ
输出:
float64:179.99999999999997
float64:179
我希望1980 * 0.1 / 1.1
的输出为180
,但实际输出为179
。
【问题讨论】:
"Floor 返回 x 的最大整数值 小于或等于。" 即来自 floor 179.9 的结果 179 是 i> 正确 (play.golang.com/p/gPPLz-nQIzW)。如果您想要 180,请改用 round 或 ceil。 啊,这是由于基点数学和舍入错误造成的。没错,实际结果应该是 180,但是浮点运算中存在舍入错误,导致它是 179.999。除以 1.1 与乘以 0.90909090 相同......这导致了您的问题。你可以谷歌“golang修复浮点错误”之类的。我也想找点东西。 @shizend 你知道 floor 做什么并且你知道 floor 之前的值是 179.999 但你仍然将问题命名为“Golang 中的楼层号不正确”。无论如何,您都不应该将浮点数精确地用于货币值,因为它们不精确。 浮点数不精确:***.com/questions/58088633/… @shizend 使用 int64 作为以美分为单位的金额是一种常见的方法。 【参考方案1】:原问题:
golang 中的楼层号不正确
使用带有浮点变量的 Math.Floor 时出现问题(圆形 向下/截断精度部分)。我怎样才能正确地做到这一点?
package main import ( "fmt" "math" ) func main() var st float64 = 1980 var salePrice1 = st * 0.1 / 1.1 fmt.Printf("%T:%v\n", salePrice1, salePrice1) var salePrice2 = math.Floor(st * 0.1 / 1.1) fmt.Printf("%T:%v\n", salePrice2, salePrice2)
我预计 1980 * 0.1 / 1.1 的输出为 180,但实际 输出为 179。”
游乐场:
输出:
float64:179.99999999999997 float64:179
XY 问题是询问您尝试的解决方案,而不是您的实际问题:The XY Problem。
显然,这是salePrice1
的金钱计算。货币计算使用精确的十进制计算,而不是不精确的二进制浮点计算。
货币计算使用整数。例如,
package main
import "fmt"
func main()
var st int64 = 198000 // $1980.00 as cents
fmt.Printf("%[1]T:%[1]v\n", st)
fmt.Printf("$%d.%02d\n", st/100, st%100)
var n, d int64 = 1, 11
fmt.Printf("%d, %d\n", n, d)
var salePrice1 int64 = (st * n) / d // round down
fmt.Printf("%[1]T:%[1]v\n", salePrice1)
fmt.Printf("$%d.%02d\n", salePrice1/100, salePrice1%100)
var salePrice2 int64 = ((st*n)*10/d + 5) / 10 // round half up
fmt.Printf("%[1]T:%[1]v\n", salePrice2)
fmt.Printf("$%d.%02d\n", salePrice2/100, salePrice2%100)
var salePrice3 int64 = (st*n + (d - 1)) / d // round up
fmt.Printf("%[1]T:%[1]v\n", salePrice1)
fmt.Printf("$%d.%02d\n", salePrice3/100, salePrice3%100)
游乐场:https://play.golang.org/p/HbqVJUXXR-N
输出:
int64:198000
$1980.00
1, 11
int64:18000
$180.00
int64:18000
$180.00
int64:18000
$180.00
参考资料:
What Every Computer Scientist Should Know About Floating-Point Arithmetic
How should we calc money (decimal, big.Float)
General Decimal Arithmetic
【讨论】:
也是一个很好的答案,通过避免使用浮点数进行货币计算来解决这个问题。 ?? 相关(但只是因为 Go 指定了二进制浮点),有一个 IEEE 十进制浮点标准。见en.wikipedia.org/wiki/Decimal_floating_point @torek:那是 Mike Cowlishaw 的项目:General Decimal Arithmetic。 我一直在玩十进制算术(断断续续):在 2.x 时代,我在 Python 中用decimal.Decimal
编写了一些实验性的货币价值处理东西,并看到了IBM 工作。虽然没有跟上最新的;上面那个链接很好。
@torek:不幸的是,英特尔和其他公司对硬件实现不感兴趣。【参考方案2】:
试试this:
st := 1980.0
f := 0.1 / 1.1
salePrice1 := st * f
salePrice2 := math.Floor(salePrice1)
fmt.Println(salePrice2) // 180
这是一个很大的话题: 对于会计系统:答案是Floating point error mitigation。
(注意:一种缓解技术是使用int64
、uint64
或big.Int
)
请看:What Every Computer Scientist Should Know About Floating-Point Arithmetic https://en.wikipedia.org/wiki/Double-precision_floating-point_format https://en.wikipedia.org/wiki/IEEE_floating_point
让我们开始吧:
fmt.Println(1.0 / 3.0) // 0.3333333333333333
IEEE 754 二进制表示:
fmt.Printf("%#X\n", math.Float64bits(1.0/3.0)) // 0X3FD5555555555555
IEEE 754 1.1 的二进制表示:
fmt.Printf("%#X\n", math.Float64bits(1.1)) // 0X3FF199999999999A
fmt.Printf("%#X\n", math.Float64bits(st*0.1/1.1)) // 0X40667FFFFFFFFFFF
现在,让:
st := 1980.0
f := 0.1 / 1.1
f
的 IEEE 754 二进制表示是:
fmt.Printf("%#X\n", math.Float64bits(f)) // 0X3FB745D1745D1746
还有:
salePrice1 := st * f
fmt.Println(salePrice1) // 180
fmt.Printf("%#X\n", math.Float64bits(salePrice1)) // 0X4066800000000000
salePrice2 := math.Floor(salePrice1)
fmt.Printf("%#X\n", math.Float64bits(salePrice2)) // 0X4066800000000000
在计算机上处理浮点数与使用笔和纸不同(浮点计算错误):
var st float64 = 1980
var salePrice1 = st * 0.1 / 1.1
fmt.Println(salePrice1) // 179.99999999999997
salePrice1
是 179.99999999999997 而不是 180.0 所以小于或等于 179.99999999999997 的整数值是 179:
查看func Floor(x float64) float64
的文档:
Floor 返回小于或等于 x 的最大 整数 值。
见:
fmt.Println(math.Floor(179.999)) // 179
fmt.Println(math.Floor(179.5 + 0.5)) // 180
fmt.Println(math.Floor(179.999 + 0.5)) // 180
fmt.Println(math.Floor(180.0)) // 180
一些相关的质量保证:
Golang floating point precision float32 vs float64
How to change a float64 number to uint64 in a right way?
Golang converting float64 to int error
Is floating point math broken?
Go float comparison
What does "%b" do in fmt.Printf for float64 and what is Min subnormal positive double in float64 in binary format?
Is there any standard library to convert float64 to string with fix width with maximum number of significant digits?
fmt.Printf with width and precision fields in %g behaves unexpectedly
Why is there a difference between floating-point multiplication with literals vs. variables in Go?
Golang Round to Nearest 0.05
【讨论】:
问题是没有使用地板。问题是 1980 * 0.1 / 1.1 问题的答案是 180。这里的结果不是由于使用了 floor 而是由于浮点错误。 @Fogmeister:不,salePrice1
是 179.99999999999997 而不是 180.0
只是因为浮点错误。用笔和纸来做。你会看到它应该是 180。另外......我不是一个沉默的反对者。我添加了一条评论。而你还是错了。 1980 * 0.1 = 198
。 198 / 1.1 = 180
。代码以 179.999... 出现的事实是由于浮点错误。
@A.R:你的答案是错误的:What Every Computer Scientist Should Know About Floating-Point Arithmetic.
@peterSO:你能详细说明一下,哪一部分是错的?这是 Golang 代码 var st float64 = 1980 var salePrice1 = st * 0.1 / 1.1 fmt.Println(salePrice1) // 179.99999999999997
不是我的。以上是关于IEEE 754 二进制浮点数不精确的主要内容,如果未能解决你的问题,请参考以下文章