相关文章推荐
满身肌肉的鸭蛋  ·  【Unity ...·  8 月前    · 
无聊的杨桃  ·  XmlSerializer ...·  1 年前    · 
大方的泡面  ·  fencedframe 可以替代 ...·  1 年前    · 

Go语言多精度浮点数的选择与思路

在我们看不到地方有一只手,而你看到的只是黑暗 ——— 弗洛伊德

尽管浮点数的表达和计算可能遇到精度问题,但是在一般场景下,这种轻微的损失基本可以忽略不计,Go语言内置的int64、float32类型可以满足大部分场景的需求。在一些比较特殊的场景下,例如加密、数据库、银行、外汇等领域需要更高精度的存储和计算时,可以使用math/big标准库,著名区块链项目以太坊即用该库来实现货币的存储和计算。math/big标准库提供了处理大数的三种数据类型: big.Int 、big.Float、big.Rat,这些数据类型在Go语言编译时的常量计算中也被频繁用到。其中, big.Int 用于处理大整数,其结构如下所示。

type Int struct {    
			neg bool // 符号    
			abs nat  // 整数位 
type nat []Word
type Word uint

big.Int 的核心思想是用uint切片来存储大整数,可以容纳的数据超过了int64的大小,甚至可以认为是无限扩容的。

大数运算和普通int64相加或相乘不同的是,大数运算需要保留并处理进位。Go语言对大数运算进行了必要的加速,例如大整数乘法运算使用了Karatsuba 算法,另外,执行运算时采用汇编代码。汇编代码与处理器架构有关,位于arith_$GOARCH.s文件中。如下例计算出第一个大于10^99的斐波那契序列的值。在该示例中,使用big.NewInt函数初始化 big.Int ,使用big.Exp函数计算大小,使用big.Cmp函数比较大整数的值,使用big.Add函数计算大整数的加法。

func main() {
   a := big.NewInt(0)
   b := big.NewInt(1)
   var limit big.Int
   limit.Exp(big.NewInt(10), big.NewInt(99), nil)
   for a.Cmp(&limit) < 0 {
      a.Add(a, b)
      a, b = b, a
   fmt.Println(a)
}

big.Float 离不开大整数的计算,其结构如下,其中,prec代表存储数字的位数,neg 代表符号位,mant代表大整数,exp代表指数。

type Float struct {
   prec uint32
   mode RoundingMode
   acc  Accuracy
   form form
   neg  bool
   mant nat
   exp  int32
}

big.Float的核心思想是把浮点数转换为大整数运算。举一个简单的例子,十进制数12.34可以表示为1234×10^-2,56.78可以表示为5678×10^2,那么有,12.34×56.78 = 1234×5678×10^-4,从而将浮点数的运算转换为了整数的运算。但是一般不能用uint64来模拟整数运算,因为整数运算存在溢出问题,因此big.Float仍然依赖大整数的运算。

需要注意的是,big.Float仍然会损失精度,因为有限的位数无法表达无限的小数。但是可以通过设置prec存储数字的位数来提高精度。prec设置得越大,其精度越高,但是相应地,在计算中花费的时间就越多,因此在实际中需要权衡prec的大小。当prec设置为53时,其精度与float64相同,而在Go编译时常量运算中,为了保证高精度,prec会被设置为数百位。

package main
import (
   "fmt"
   "math/big"
func main() {
   var x1,y1 float64 = 10,3
   z1 := x1/y1
   fmt.Println(x1, y1, z1)
   x2, y2 := big.NewFloat(10), big.NewFloat(3)
   z2 := new(big.Float).Quo(x2, y2)
   fmt.Println(x2, y2, z2)
}

在上例中,x2,y2通过big.NewFloat初始化。当不设置prec时,其精度与float64相同。如上例中,x2,y2最终打印出的结果与x1,y1是完全一致的。

10 3 3.3333333333333335
10 3 3.3333333333333335

当把x2,y2的prec位数设置为100时,如下所示,可以看到,打印出的浮点数精度有明显的提升。

x2, y2 := big.NewFloat(10), big.NewFloat(3)
z2 := new(big.Float).Quo(x2, y2)
x2.SetPrec(100)
y2.SetPrec(100)
// 输出: 10 3 3.333333333333333333333333333332
fmt.Println(x2, y2, z2)

如果希望有理数计算不丢失精度,那么可以借助big.Rat实现。big.Rat仍然依赖大整数运算,其结构如下所示,其中,a、b分别代表分子和分母。

type Rat struct {
   a, b big.Int
}

big.Rat的核心思想是将有理数的运算转换为分数的运算。例如12/34×56/78 = (12×78 + 34 ×56) / (34 ×78) ,最后分子分母还需要进行约分。通过将有理数的运算转换为分数的运算将永远不会损失精度。如下所示,最终打印出的结果为z:1/3。

func main() {
   x, y := big.NewRat(1,2), big.NewRat(2,3)