浮点数计算不精确

浮点数与整数计算

先看两个case

1
2
3
4
5
6
7
// case1: 135.90*100 ====
// float32
var f1 float32 = 135.90
fmt.Println(f1 * 100) // output:13589.999
// float64
var f2 float64 = 135.90
fmt.Println(f2 * 100) // output:13590

浮点数在单精度下, 135.9*100即出现了偏差, 双精度下结果正确.

合理但须注意: 两位小数乘100强转int后输出, 比期望值少了1.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main
import (
	"fmt"
)
func main() {
	// case: int64==>float64
	var c int64 = 987654321098765432
	fmt.Printf("%.f\n", float64(c)) //output:987654321098765440
	// case: int(float64(xx.xx*100))
	var d float64 = 1129.6
	var e int64 = int64(d * 100)
	fmt.Println(e) //output:112959
}

浮点数与浮点数计算

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// case2: 0.1 add 10 times ===
// float32
var f3 float32 = 0
for i := 0; i < 10; i++ {
	f3 += 0.1
}
fmt.Println(f3) //output:1.0000001
// float64
var f4 float64 = 0
for i := 0; i < 10; i++ {
	f4 += 0.1
}
fmt.Println(f4) //output:0.9999999999999999

0.1加10次, 这下无论是float32和float64都出现了偏差.

为什么呢, Go和大多数语言一样, 使用标准的IEEE754表示浮点数, 0.1使用二进制表示结果是一个无限循环数, 只能舍入后表示, 累加10次之后就会出现偏差.

int转float

int64转float64在数值很大的时候出现偏差.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

import (
	"fmt"
)

func main() {
	// case: int64==>float64
	var c int64 = 987654321098765432
	fmt.Printf("%.f\n", float64(c)) //output:987654321098765440
}

浮点数的截取原则

问题代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
	"fmt"
	"strconv"
)

func main() {
	var ff, e float64
	e = 100.00
	ff = -0.210615789

	ff = FloatRound(ff, 4)
	fmt.Println(ff)  // 输出 -0.2106
	qq := ff * e
	fmt.Println(qq)  // 输出 -21.060000000000002
}

// 截取小数位数
func FloatRound(f float64, n int) float64 {
	format := "%." + strconv.Itoa(n) + "f"
	res, _ := strconv.ParseFloat(fmt.Sprintf(format, f), 64)
	return res
}

可以看到qq的输出,并不是我们想到那样是-21.06.

所以以后对于浮点数的截取动作一定要放在运算之后。正确代码如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
	"fmt"
	"strconv"
)

func main() {
	var ff, e float64
	e = 100.00
	ff = -0.210615789

    // 先计算
	qq := ff * e
	fmt.Println(qq)  // 输出 -21.0615789
    // 再截取
	qq = FloatRound(qq, 4)
	fmt.Println(qq)  // 输出 -21.0616

}

// 截取小数位数
func FloatRound(f float64, n int) float64 {
	format := "%." + strconv.Itoa(n) + "f"
	res, _ := strconv.ParseFloat(fmt.Sprintf(format, f), 64)
	return res
}

float32和float64的区别

float32是一个32位数字 - float64使用64位。

这意味着float64占用了两倍的内存 - 在某些机器架构中对它们进行操作可能会慢得多。

但是,float64可以比32位浮点数更准确地表示数字。

它们还允许存储更大的数字……尽管这很少是一个问题。

通常,您应该在极其高精度或非常小的内存占用不太重要的情况下使用“float” - 并让编译器决定您的特定硬件(通常为“float32”)的效率最高。

我个人的经验法则是使用float,除非我需要“百万分之一”范围或更好的精度 - 在这种情况下我使用float64。

一些系统(例如显卡上的GPU处理器)也可能支持float16(也称为“半浮动”),其精度极差 - 但在较旧的GPU上可能要快得多。

其他系统具有“long double”,这是一个80位浮点表示,比float64具有更高的精度(更多内存,可能更慢)。

有时候我们会发现float32和float64直接互转会精度丢失, 四舍五入后错误.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

import (
    "fmt"
)

func main() {
    var a float32 = 359.9
    fmt.Println(a)
    fmt.Println(float64(a))
}

你永远不会从一个float32到float64的转换时丢失精度。前者必须是后者的一个子集。

它更多地与输出格式化程序的默认精度有关。

最接近的IEEE754 float到359.9是

1
359.899993896484375

最接近的IEEE754 float64到359.9是

1
359.8999999999999772626324556767940521240234375

最近的IEEE754 float64到359.899993896484375是

1
359.899993896484375

(即是相同的;由于我已经提到的子集规则)。

所以你可以看到它float64(a)和float64(359.899993896484375)它是一样的359.899993896484375。这解释了输出,虽然您的格式化程序正在舍入最后的2位数。

这有助于我们理解上面的结论

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package main

import (
	"fmt"
)

func main() {
	var a float32 = 359.9
	fmt.Printf("%b\n", a)
	fmt.Printf("%e\n", a)
	fmt.Printf("%f\n", a)
	fmt.Printf("%g\n", a)
	fmt.Printf("%.15f\n", a)
}
1
2
3
4
5
11793203p-15
3.599000e+02
359.899994
359.9
359.899993896484375

默认舍入规则——四舍六入五成双

四舍六入五成双是一种比较精确比较科学的计数保留法,是一种数字修约规则,又名银行家舍入法。它比通常用的四舍五入法更加精确。

具体规则:

  1. 被修约的数字小于5时,该数字舍去;

  2. 被修约的数字大于5时,则进位;

  3. 被修约的数字等于5时,要看5前面的数字,若是奇数则进位,若是偶数则将5舍掉,即修约后末尾数字都成为偶数;若5的后面还有不为“0”的任何数,则此时无论5的前面是奇数还是偶数,均应进位。

助记口诀:

四舍六入五考虑,五后非零就进一,五后为零看奇偶,五前为偶应舍去,五前为奇要进一

Golang中浮点型默认使用银行家舍入法,如下使用代码验证示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import (
	"fmt"
)

func main() {
	fmt.Printf("9.8249	=>	%0.2f(四舍)\n", 9.8249)
	fmt.Printf("9.82671	=>	%0.2f(六入)\n", 9.82671)
	fmt.Printf("9.8351	=>	%0.2f(五后非零就进一)\n", 9.8351)
	fmt.Printf("9.82501	=>	%0.2f(五后非零就进一)\n", 9.82501)
	fmt.Printf("9.8250	=>	%0.2f(五后为零看奇偶,五前为偶应舍去)\n", 9.8250)
	fmt.Printf("9.8350	=>	%0.2f(五后为零看奇偶,五前为奇要进一)\n", 9.8350)
}

输出结果

1
2
3
4
5
6
9.8249  =>  9.82(四舍)
9.82671 =>  9.83(六入)
9.8351  =>  9.84(五后非零就进一)
9.82501 =>  9.83(五后非零就进一)
9.8250  =>  9.82(五后为零看奇偶,五前为偶应舍去)
9.8350  =>  9.84(五后为零看奇偶,五前为奇要进一)

因此,我可以方便得使用fmt的方法对浮点型进行银行家取舍,取得其近似数。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import (
	"fmt"
	"strconv"
)

func main() {
	s := fmt.Sprintf("%0.6f", 17.82671567890123456789987654324567898765432)
	f, _ := strconv.ParseFloat(s, 64)
	fmt.Println(s, f)
}

输出结果

1
17.826716 17.826716

须知:Golang中浮点数精确到超过14位小数后,该舍入规则将不准确,原因是golang的浮点型最大精确到小数点后15位!

其他舍入方法

要注意的是,取完整后返回的并不是真正的整数,而是float64 类型,所以如果需要int 类型的话需要手动转换。

func Ceil

1
func Ceil(x float64) float64

返回不小于x的最小整数(的浮点值),特例如下:

1
2
3
Ceil(±0) = ±0
Ceil(±Inf) = ±Inf
Ceil(NaN) = NaN

func Floor

1
func Floor(x float64) float64

返回不大于x的最大整数(的浮点值),特例如下:

1
2
3
Floor(±0) = ±0
Floor(±Inf) = ±Inf
Floor(NaN) = NaN

func Trunc

1
func Trunc(x float64) float64

返回x的整数部分(的浮点值)。特例如下:

1
2
3
Trunc(±0) = ±0
Trunc(±Inf) = ±Inf
Trunc(NaN) = NaN

func Round

1
func Round(x float64) float64

返回x的四舍五入值后的整数部分(的浮点值)

特例如下:

1
2
3
Round(±0) = ±0
Round(±Inf) = ±Inf
Round(NaN) = NaN

四舍五入保留小数点

Round

math.Round()函数四舍五入到最接近的整数(基本上是"四舍五入至1.0"运算),使用该函数,我们可以很容易地构造一个四舍五入到所选单位的函数:

1
2
3
func Round(x, unit float64) float64 {
    return math.Round(x/unit) * unit
}

测试它:

1
2
3
4
5
6
fmt.Println(Round(0.363636, 0.05)) // 0.35
fmt.Println(Round(3.232, 0.05))    // 3.25
fmt.Println(Round(0.4888, 0.05))   // 0.5
fmt.Println(Round(-0.363636, 0.05)) // -0.35
fmt.Println(Round(-3.232, 0.05))    // -3.25
fmt.Println(Round(-0.4888, 0.05))   // -0.5

请注意,unit可以是"任意"数字。如果它是1,则Round()基本上四舍五入为最接近的整数。如果为10,则四舍五入为十进制;如果为0.01,则四舍五入为2个小数位。

负数

要注意,当您用负数调用Round()时,结果如下:

1
2
3
fmt.Println(Round(-0.363636, 0.05)) // -0.35
fmt.Println(Round(-3.232, 0.05))    // -3.25
fmt.Println(Round(-0.4888, 0.05))   // -0.5

可以看到,负数的保留位数的方法和正数相同.

有限精度

请注意,用unit=0.05四舍五入3.232不会完全打印3.25,而是打印0.35000000000000003。这是因为float64数字是使用称为IEEE-754标准的有限精度存储的。

由于您不喜欢不精确的0.35000000000000003,因此建议将其格式化并重新解析为:

1
formatted, err := strconv.ParseFloat(fmt.Sprintf("%.2f", rounded), 64)

而且,这种"貌似"的结果与打印出来的结果完全一样,它给出了0.35。

例如:

1
2
3
4
5
6
7
8
9
func main() {
    fmt.Printf("%.3f", Round(0.363636, 0.05))
    fmt.Printf("%.3f", Round(3.232, 0.05))
    fmt.Printf("%.3f", Round(0.4888, 0.05))
}

func Round(x, unit float64) float64 {
    return float64(int64(x/unit+0.5)) * unit
}

它将是准确的(在Go Playground上尝试):

1
2
3
0.350
3.250
0.500

但这只是一个"幻想"。由于0.35不能使用IEEE-754标准用有限的位表示,因此对数字无所谓,如果将其存储在类型为float64的值中,则它不会完全是0.35(但IEEE-754编号非常接近)。您看到的是fmt.Println()将其打印为0.35,因为fmt.Println()已经进行了近似算法。

但是,如果您尝试以更高的精度打印它:

1
2
3
fmt.Printf("%.30f", Round(0.363636, 0.05))
fmt.Printf("%.30f", Round(3.232, 0.05))
fmt.Printf("%.30f", Round(0.4888, 0.05))

输出:

1
2
3
0.349999999999999977795539507497
3.250000000000000000000000000000
0.500000000000000000000000000000

请注意,另一方面,3.25和0.5是精确的,因为它们可以精确地用有限的位表示,因为用二进制表示:

1
2
3.25 = 3 + 0.25 = 11.01binary
0.5 = 0.1binary

降低精度损失的办法

在 Token 和时间的相互转化函数 durationFromTokens 和 tokensFromDuration 中,涉及到 float64 的乘除运算。一谈到 float 的乘除,我们就需要小心精度问题了。

Golang 在获取指定期间d内产生的令牌数量里也踩了坑,以下是 tokensFromDuration 最初的实现版本

1
2
3
func (limit Limit) tokensFromDuration(d time.Duration) float64 {
	return d.Seconds() * float64(limit)
}

这个操作看起来一点问题都没:每秒生成的 Token 数乘于秒数。

然而,这里的问题在于,d.Seconds() 已经是小数了。两个小数相乘,会带来精度的损失。

修改后新的版本如下:

1
2
3
4
5
6
7
8
9
// tokensFromDuration is a unit conversion function from a time duration to the number of tokens
// which could be accumulated during that duration at a rate of limit tokens per second.
func (limit Limit) tokensFromDuration(d time.Duration) float64 {
	// Split the integer and fractional parts ourself to minimize rounding errors.
	// See golang.org/issues/34861.
	sec := float64(d/time.Second) *float64(limit)
	nsec := float64(d%time.Second)* float64(limit)
	return sec + nsec/1e9
}

高精度小数计算

Go语言的big包实现大整数运算

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
    // bigint project main.go
    package main

    import (
        "fmt"
        "math"
        "math/big"
    )

    func main() {
        // Here are some calculations with bigInts:
        im := big.NewInt(math.MaxInt64)
        in := im
        io := big.NewInt(1956)
        ip := big.NewInt(1)
        ip.Mul(im, in).Add(ip, im).Div(ip, io)
        fmt.Printf("Big Int: %v\n", ip)
        iq := big.NewInt(10000)
        ip.Mod(ip, iq)
        fmt.Printf("Big Int: %v\n", ip)
    }

程序说明:

1.math包中包含有各种功能函数,包括最大的整数math.MaxInt64

2.math/big包可以用于大整数计算

3.大整数可以使用"%v"格式输出

另外,显式初始化一个大数只能到上限math.MaxInt64,如果想设置比这还大的数,则只能使用如下方法,即从byte到big int:

Convert byte array to big.Int

1
2
3
4
import "math/big"

z := new(big.Int)
z.SetBytes(byteSliceHere)

or:

1
2
3
4
5
6
7
8
9
func Base64ToInt(s string) (*big.Int, error) {
    data, err := base64.StdEncoding.DecodeString(s)
    if err != nil {
        return nil, err
    }
    i := new(big.Int)
    i.SetBytes(data)
    return i, nil
}

Convert String to big.Int

使用 math/big 的内置函数:

1
func (z *Int) SetString(s string, base int) (*Int, bool)

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

import (
    "fmt"
    "math/big"
)

func main() {
    n := new(big.Int)
    n, ok := n.SetString("314159265358979323846264338327950288419716939937510582097494459", 10)
    if !ok {
        fmt.Println("SetString: error")
        return
    }
    fmt.Println(n)
}

Convert a bigint to a string in Go

1
2
bigint := big.NewInt(123)
bigstr := bigint.String()

输出形式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package main

    import (
     "fmt"
     "math/big"
    )

    func main() {
     const prec = 200
     a := new(big.Float).SetPrec(prec).SetFloat64(5000.0)
     b := new(big.Float).SetPrec(prec).SetFloat64(4000.30)
     result := new(big.Float).Sub(a, b)
     fmt.Println(result)
    }
1
Result: 999.6999999999998181010596454143524169921875

我们会发现结果出现了一些误差,这个偏差是因为fmt.Println直接输出result引起的,为了避免这个问题,需要使用func (x *Float) String() string

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import (
    "fmt"
    "math/big"
)

func main() {
    const prec = 200
    a, _ := new(big.Float).SetPrec(prec).SetString("5000")
    b, _ := new(big.Float).SetPrec(prec).SetString("4000.30")
    result := new(big.Float).Sub(a, b)
    fmt.Println(result)
    fmt.Println(result.String())
}

输出:

1
2
999.6999999999999999999999999999999999999999999999999999999995
999.7

参考: https://my.oschina.net/henrylee2cn/blog/741753 https://stackoverflow.com/questions/39642810/why-am-i-losing-precision-while-converting-float32-to-float64 https://www.cnblogs.com/welhzh/p/8981096.html