讨论系统中?钱的精度问题

作者 : 开心源码 本文共4437个字,预计阅读时间需要12分钟 发布时间: 2022-05-14 共179人阅读

来自公众号:Gopher指北

钱,乃亘古之玄物,有则气粗神壮,缺则心卑力浅

在一个系统中,特别是一个和钱相关的系统,钱乃重中之重,计算时的精度将是本篇探讨的主题。

精度为何如此重要

“积羽沉舟”用在此处最为合适。如果某电商平台每年订单成交数量为10亿,每笔订单少结算1分钱,则累计损失1000万!有一说一,这损失的钱就是王某人的十分之一个小目标。假如由于精度问题在给用户结算时,少算会损失用户,多算会损失钱。由此可见,准确的计算钱十分重要!

为什么会有精度的问题

经典案例,我们来看一下0.1 + 0.2在计算机中能否等于0.3

image

上述case学过计算机的应该都知道,计算机是二进制的,用二进制表示浮点数时(IEEE754标准),只有一些的数可以用这种方法准确的表示出来。下面以0.3为例看一下十进制转二进制小数的过程。

image

计算机的位数有限制,因而计算机用浮点数计算时一定无法得到准确的结果。这种硬限制无法突破,所以需要引入精度以保证对钱的计算在允许的误差范围内尽可能精确。

关于浮点数在计算机中的实际表示本文不做进一步探讨,可以参考下述连接学习:

单精度浮点数表示:

https://en.wikipedia.org/wiki/Single-precision_floating-point_format

双精度浮点数表示:

https://en.wikipedia.org/wiki/Double-precision_floating-point_format

浮点数转换器:

https://www.h-schmidt.net/FloatConverter/IEEE754.html

用浮点数计算

还是以上述0.1 + 0.2为例,0.00000000000000004的误差完全可以忽略,我们尝试小数部分保留5位精度,看下面结果。

image

此时的结果符合预期。这也是为什么很多时候判断两个浮点数能否相等往往采用a - b <= 0.00001的形式,说白了这就是小数部分保留5位精度的另一种体现形式。

用整型计算

前面提到只有一些的浮点数可以用IEEE754标准表示,而整型可准确表示所有有效范围内的数。因而很容易想到,使用整型表示浮点数。

例如,事前定好小数保留8位精度,则0.10.2分别表示成整数为1000000020000000, 浮点数的运算也就转换为整型的运算。还是以0.1 + 0.2为例。

// 表示小数位保留8位精度const prec = 100000000func float2Int(f float64) int64 {    return int64(f * prec)}func int2float(i int64) float64 {    return float64(i) / prec}func main() {    var a, b float64 = 0.1, 0.2    f := float2Int(a) + float2Int(b)    fmt.Println(a+b, f, int2float(f))    return}

上述代码输出结果如下:

image

上述输出结果完全符合预期,所以用整型来表示浮点数看起来是一个可行的方案。但,我们不能局限于个例,还需要更多的测试。

fmt.Println(float2Int(2.3))

上述代码输出结果如下:

image

这个结果是如此的出乎预料,却又是情理之中。

image

上图表示2.3在计算机中实际的存储值,因而使用float2Int函数进行转换时的结果是229999999而不是230000000

这个结果很显著不符合预期,在确定的精度范围内仍有精度损失,假如把这个代码发到线上,很大概率第二天就会光速离任。要处理这个问题也很简单,只要引入github.com/shopspring/decimal就可,看下面修正后的代码。

// 表示小数位保留8位精度const prec = 100000000var decimalPrec = decimal.NewFromFloat(prec)func float2Int(f float64) int64 {    return decimal.NewFromFloat(f).Mul(decimalPrec).IntPart()}func main() {    fmt.Println(float2Int(2.3)) // 输出:230000000}

此时结果符合预期,系统内部的浮点运算(加法、减法、乘法)均可转换为整型运算,而运算结果只要要一次浮点转换就可。

到这里,用整型计算基本能满足大部分场景,但仍有两个问题尚需注意。

1、整型表示浮点数的范围能否满足系统需求。

2、整型表示浮点数时除法仍旧需要转换为浮点数运算。

整型表示浮点数的范围

int64为例,数值范围为-9223372036854775808~9223372036854775807,假如我们对小数部分精度保留8位,则剩余表示整数部分仍旧有11位,即只表示钱的话依旧可以存储上百亿的金额,这个数值对很多系统和中小型公司而言已经绰绰有余,但是使用此方式存储金额时范围仍旧是需要慎重考虑的问题。

整型表示浮点数的除法

在Go中没有隐式的整型转浮点的说法,即整型和整型相除得到的结果仍旧是整型。我们以整型表示浮点数时,就尤其需要注意整型的除法运算会丢失所有的小数部分,所以肯定要先转换为浮点数再进行相除。

浮点和整型的最大精度

int64的范围为-9223372036854775808~9223372036854775807,则用整型表示浮点型时,整数部分和小数部分的有效十进制位最多为19位。

uint64的范围为0~18446744073709551615,则用整型表示浮点型时,整数部分和小数部分的有效十进制位最多为20位,由于系统中表示金额时一般不会存储负数,所以和int64相比,更加推荐使用uint64

float64根据IEEE754标准,并参考维基百科知其整数部分和小数部分的有效十进制位为15-17位。

image

我们看下面的例子。

var (    a float64 = 123456789012345.678    b float64 = 1.23456789012345678)fmt.Println(a, b, decimal.NewFromFloat(a), a == 123456789012345.67)return

上述代码输出结果如下:

image

根据输出结果知,float64无法表示有效位数超过17位的十进制数。从有效十进制位来讲,老许更加推荐使用整型表示浮点数。

计算中尽量保留更多的精度

前面提到了精度的重要性,以及整型和浮点型可表示的最大精度,下面我们以一个实际例子来讨论计算过程中能否要保留指定的精度。

var (    // 广告平台总共收入7.11美元    fee float64 = 7.1100    // 以下是不同渠道带来的点击数    clkDetails = []int64{220, 127, 172, 1, 17, 1039, 1596, 200, 236, 151, 91, 87, 378, 289, 2, 14, 4, 439, 1, 2373, 90}    totalClk   int64)// 计算所有渠道带来的总点击数for _, c := range clkDetails {    totalClk += c}var (    floatTotal float64    // 以浮点数计算每次点击的收益    floatCPC float64 = fee / float64(totalClk)    intTotal int64    // 以8位精度的整形计算每次点击的收益(每次点击收益转为整形)    intCPC        int64 = float2Int(fee / float64(totalClk))    intFloatTotal float64    // 以8位进度的整形计算每次点击的收益(每次点击收益保留为浮点型)    intFloatCPC  float64 = float64(float2Int(fee)) / float64(totalClk)    decimalTotal         = decimal.Zero    // 以decimal计算每次点击收益    decimalCPC = decimal.NewFromFloat(fee).Div(decimal.NewFromInt(totalClk)))// 计算各渠道点击收益,并累加for _, c := range clkDetails {    floatTotal += floatCPC * float64(c)    intTotal += intCPC * c    intFloatTotal += intFloatCPC * float64(c)    decimalTotal = decimalTotal.Add(decimalCPC.Mul(decimal.NewFromInt(c)))}// 累加结果比照fmt.Println(floatTotal) // 7.11fmt.Println(intTotal) // 710992893fmt.Println(decimal.NewFromFloat(intFloatTotal).IntPart()) // 711000000fmt.Println(decimalTotal.InexactFloat64()) // 7.1100000000002375

比照上面的计算结果,只有第二种精度最低,而造成该精度丢失的主要起因是float2Int(fee / float64(totalClk))将中间计算结果的精度也只保留了8位,因而在结果上面产生了误差。其余计算方式在中间计算过程中尽可能的保留了精度因而结果符合预期。

除法和减法的结合

根据前面的形容,在计算除法的过程中要使用浮点数且尽可能保留更多的精度。这仍旧不能处理所有问题,我们看下面的例子。

// 1元钱分给3个人,每个人分多少?var m float64 = float64(1) / 3fmt.Println(m, m+m+m)

上述代码输出结果如下:

image

由计算结果知,每人分得0.3333333333333333元,而将每人分得的钱再次汇总时又变成了1元,那么
0.0000000000000001元是从石头里面蹦出来的嘛!有些时候我真的搞不懂这些计算机。

这个结果很显著不符合人类的直觉,为了更加符合直觉我们结合减法来完成本次计算。

// 1元钱分给3个人,每个人分多少?var m float64 = float64(1) / 3fmt.Println(m, m+m+m)// 最后一人分得的钱使用减法m3 := 1 - m - mfmt.Println(m3, m+m+m3)

上述代码输出结果如下:

image

通过减法我们终于找回了那丢失的0.0000000000000001元。当然上面仅是老许举的一个例子,在实际的计算过程中可能需要通过decimal库进行减法以保证钱不凭空消失也不凭空添加。

以上均为老许的浅薄之见,有任何疑虑和错误请及时指出,衷心希望本文能够对各位读者有肯定的帮助。

注:

写本文时, 笔者所用go版本为: go1.16.6

文章中所用部分例子: Isites/go-coder/blob/master/money/main.go

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 讨论系统中?钱的精度问题

发表回复