0%

《其他》大数以及数值精度

计算机中使用二进制保存自然界的各种数值,有正整数、负整数、正小数、负小数,二进制如何表示这四种数值呢

正整数

正整数其实不用特殊表示,直接就可以保存。例如 2 的二进制表示就是 10,8 的二进制表示就是 1000

对应编程语言中的 uint

负整数

负整数不能直接保存为二进制,而是需要一种规则来体现。规则就是:最前面一位表示符号位,0 表示正数,1 表示负数

负整数的二进制存储就是:第一位是 1,其他位是正整数的补码

补码 = 反码 + 1

反码 = 原码按位取反

原码 = 正整数的二进制存储

比如 -5 对应正数 5(x0000101),所以原码就是 x0000101

得到反码是 x1111010

加上 1,得到补码,就是 x1111011

所以 -5 的二进制存储就是 11111011

对应编程语言中的 int

int8 的范围是:[-2^7, 2^7),即 [-128, 127]

注意:特别的情况,-128 的二进制存储是:10000000

正负小数

小数的表示就更复杂了,普遍采用的规则就是 IEEE754 格式,由符号位、指数区域、小数区域组成,是科学技术法的直观表示

指数区域由 11 ( float32 位的话就是 8 ) 个 bit 组成,最高位不是表示符号位,那么怎么能表示负指数呢

规定这 11 个 bit 形成的正整数的值等于科学计数法中的指数 + 1023 (float32 位的话是 127),那么 指数 = 它的值 - 1023,这就可以成为负数

小数区域表示科学计数法中的小数部分(1.355 * 10^2 中的 0.355),由 52(float32 位的话就是 23)个 bit 组成

对应编程语言中的 float

十进制小数转换为二进制小数

方法是:乘 2 取整

如:0.625 =(0.101)B
0.6252 = 1.25 -> 取出整数部分 1
0.25
2 = 0.5 -> 取出整数部分 0
0.5*2 = 1 -> 取出整数部分 1

二进制小数转换为十进制小数

第 n 个小数位就是 2^n

0.101 = 2^(-1) + 2^(-3) = 0.5 + 1/8 = 0.625

浮点数范围

指数区域是 11 bit,所有位都是 1,最大值就是 2^11-1,也就是 2047。最小值就是 0

因为 指数区域的值 = 科学计数法中的指数 + 1023,那么科学计数法中的最小指数是 0 - 1023 = -1023,最大指数是 2047 - 1023 = 1024

所以科学计数法中的指数的范围就是 -1023 ~ 1024

而小数区域表示的是小数,其最大值是 1,所以不算符号位,浮点数的最大值是 1 * 2^1024

算上符号位,浮点数的范围就是 -2^1024 ~ 2^1024,也就是 -1.7976931348623157 × 10^308 ~ 1.7976931348623157 × 10^308

安全整数范围:

安全整数范围意思是在这个范围的整数都是精确的安全的,但是这个范围内的小数依然有可能损失精度,比如 0.1 就会损失

能安全表示的数值范围是:

−9007199254740991 到 9007199254740991 (即正负 2 的 53 次方 - 1,也就是 52 个 1 组成的二进制)

js 中 number 类型是 float64,采用 IEEE754 格式,给小数部分用的只有 52 bit,52 位能表示 −9007199254740991 到 9007199254740991,再大的话,小数部分放不下导致溢出,比较以及计算都会出问题,比如:

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
package main

import (
"bytes"
"encoding/binary"
"fmt"
"log"
)

func main() {
test(float64(9007199254740991)) // [01000011 00111111 11111111 11111111 11111111 11111111 11111111 11111111]
test(float64(9007199254740992)) // [01000011 01000000 00000000 00000000 00000000 00000000 00000000 00000000] 从二进制来看,这里已经成了1 * 2^53,不是9007199254740992了
test(float64(9007199254740993)) // [01000011 01000000 00000000 00000000 00000000 00000000 00000000 00000000]
test(float64(9007199254740994)) // [01000011 01000000 00000000 00000000 00000000 00000000 00000000 00000001]
fmt.Println(float64(9007199254740992) == float64(9007199254740993)) // true 这里做比较已经没有意义,因为9007199254740992和9007199254740993在转换的时候已经发生溢出了,溢出后正好他们相等
}

func test(a float64) {
var buffer bytes.Buffer
err := binary.Write(&buffer, binary.BigEndian, a)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%08b\n", buffer.Bytes())
}

IEEE754 格式的精度损失

在 IEEE754 格式下,一个小数一旦存入计算机就可能造成精度丢失,下次读出来就不是同一个数

因为一个十进制小数转换成二进制小数可能变成无限循环二进制小数。就跟 10/3 一样

下面都以 float64 举例

下面以 0.1 举例解释浮点误差的原因, 0.1 转成二进制表示为 0.0001100110011001100(1100循环),1.100110011001100x2^-4,所以 E = -4 + 1023 = 1019(IEEE754格式规定:E = 指数 + 1023);M 舍去首位的 1,得到 100110011…。最终就是:

0(符号位) 01111111011(指数区域,1019) 1001100110011001100…110011010(这里最后一位进了1,所以有了精度损失)

转化成十进制后为 0.100000000000000005551115123126,因此就出现了浮点误差

看下面实例演示

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
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

import (
"bytes"
"encoding/binary"
"fmt"
"log"
)

func main() {
var buffer bytes.Buffer
err := binary.Write(&buffer, binary.BigEndian, float64(0.1))
if err != nil {
log.Fatal(err)
}
fmt.Printf("%08b\n", buffer.Bytes()) // [00111111 10111001 10011001 10011001 10011001 10011001 10011001 10011010]
var a float64
err = binary.Read(bytes.NewReader(buffer.Bytes()), binary.BigEndian, &a)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%.20f\n", a) // 0.10000000000000000555

fmt.Printf("%.20f\n", float64(0.1) + float64(0.2)) // 0.30000000000000004441

fmt.Printf("%.20f\n", float64(0.1) + float64(0.5)) // 0.59999999999999997780

fmt.Printf("%.100f\n", float64(0.1))
fmt.Printf("%.100f\n", float64(0.2))
fmt.Printf("%.100f\n", float64(0.1) + float64(0.2))
fmt.Printf("%.100f\n", float64(0.3))
fmt.Println(float64(0.1) + float64(0.2) == float64(0.3)) // false

fmt.Printf("%.100f\n", float32(0.1))
fmt.Printf("%.100f\n", float32(0.2))
fmt.Printf("%.100f\n", float32(0.1) + float32(0.2))
fmt.Printf("%.100f\n", float32(0.3))
fmt.Println(float32(0.1) + float32(0.2) == float32(0.3)) // true
fmt.Println(float32(0.11) + float32(0.22) == float32(0.33)) // false
}

一个小数的存取都会发生精度损失,计算同样也会,所以,不要使用浮点型进行运算,可以使用官方的 BigFloat 或者 go-decimal 三方库(github.com/pefish/go-decimal)

0.1+0.2!=0.3

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
28
29
30
31
32
33
34
35
package main

import (
"bytes"
"encoding/binary"
"fmt"
"log"
)

func main() {
var b bytes.Buffer
err := binary.Write(&b, binary.BigEndian, float64(0.1))
if err != nil {
log.Fatal(err)
}
fmt.Printf("0.1 : %08b\n", b.Bytes())
b.Reset()
err = binary.Write(&b, binary.BigEndian, float64(0.2))
if err != nil {
log.Fatal(err)
}
fmt.Printf("0.2 : %08b\n", b.Bytes())
b.Reset()
err = binary.Write(&b, binary.BigEndian, float64(0.1) + float64(0.2))
if err != nil {
log.Fatal(err)
}
fmt.Printf("0.1 + 0.2: %08b\n", b.Bytes())
b.Reset()
err = binary.Write(&b, binary.BigEndian, float64(0.3))
if err != nil {
log.Fatal(err)
}
fmt.Printf("0.3 : %08b\n", b.Bytes())
}

执行上面程序,会发现 0.1+0.2 确实不等于 0.3

0.1 有精度损失,0.2 也有精度损失,两者相加,结果必然也有精度损失,0.3 也有精度损失

0.1 + 0.2 的计算结果精度损失后显然跟 0.3 损失后不相等

想想:0.1 最后 0 舍 1 入(对应 10 进制的四舍五入)后变大了,0.2 同样变大了,两者相加变得比 0.3 更大,但是 0.3 0 舍 1 入后却变得比 0.3 还小了,所以 0.1+0.2!=0.3

跟十进制做对比比较好理解:

本来 1.156 + 1.256 = 2.412,但是我们假设精度只能是 2,那么 1.156 四舍五入变成 1.16,1.256 变成 1.26,那么 1.16+1.26=2.42,但是 2.412 四舍五入却变成了 2.41,所以在精度 2 的前提下,1.156+1.256=2.412 不成立

0.1+0.3==0.4

0.1 和 0.3 都有精度损失,为什么 0.1+0.3==0.4 是成立的呢?

虽然 0.1 和 0.3 都有精度损失,但是 0.4 也有精度损失,他们损失下来刚好相等

还是举个十进制的例子:

本来 1.159+1.259=2.418,但是我们假设精度只能是 2,那么 1.159 四舍五入变成 1.16,1.259 变成 1.26,那么 1.16+1.26=2.42,而 2.418 四舍五入也变成了 2.42,所以在精度 2 的前提下,1.159+1.259=2.418 也成立

解决大数问题

Javascript 中的 BigInt 是一个新类型,相当于 uint1024

Golang 中同样也有 BigInt 类型

位运算

>><< 都是算数位移,相对的有逻辑位移。算数位移对负数也成立,逻辑位移不能用于负数运算。

对于左移,两者没有差别,对于右移,算法不一样

逻辑右移:右移一位,左边补零即可
算术右移:右移一位,若符号位为 1,就在左边补 1,否则补 0。只有这样 -8 >> 2 才等于 -8/2^2 = -2

在 Javascript 中,数组索引还有位操作被限制为 int32 类型,范围是正负 2 的 31 次方

Golang 中:

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

import (
"fmt"
)

func main() {
fmt.Println(1 << 31) // 2147483648 无溢出,因为int64可以装下1 << 31
fmt.Println(1 << 32) // 4294967296 无溢出
}

Javascript中:

1
2
3
4
5
6
7
console.log(1 << 31)  // -2147483648  int32的最小值,没有溢出

console.log(1 << 32) // 1 循环溢出,形成一个圈。最前面一个1跑到最后面去了,变成了 0000...00001

console.log(1 << 33) // 2 变成了 0000...000010

console.log(1 << 33) // 4 变成了 0000...000100

strconv.FormatFloat 中第 3 个参数

strconv.FormatFloat 是将 float 数据转化成 string,使用 Grisu3 算法

其中第 3 个参数是保留的精度,如果填负数,则按照二进制精度 52 进行 0 舍 1 入,转化成安全值

1
fmt.Println(strconv.FormatFloat(float64(9.0071992547409911), 'f', -1, 64))  // 9.007199254740991



微信关注我,及时接收最新技术文章