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。

(注意:一种缓解技术是使用int64uint64big.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 = 198198 / 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 二进制浮点数不精确的主要内容,如果未能解决你的问题,请参考以下文章

IEEE754表示浮点数

IEEE 754的简介

IEEE 754 浮点数的表示精度探讨

IEEE754 浮点数

IEEE754浮点数表示法中阶码的范围是多少?

IEEE754二进制浮点数算术标准