浮点数计算不精确
浮点数与整数计算
先看两个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是
最接近的IEEE754 float64到359.9是
1
|
359.8999999999999772626324556767940521240234375
|
最近的IEEE754 float64到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
|
默认舍入规则——四舍六入五成双
四舍六入五成双是一种比较精确比较科学的计数保留法,是一种数字修约规则,又名银行家舍入法。它比通常用的四舍五入法更加精确。
具体规则:
-
被修约的数字小于5时,该数字舍去;
-
被修约的数字大于5时,则进位;
-
被修约的数字等于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)
}
|
输出结果
须知: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上尝试):
但这只是一个"幻想"。由于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