浅谈float浮点型的底层存储与运算
1、无中生“友”
我有一个“朋友”,正在学习第二门语言时遇到这样一个现象
package main
import (
"fmt"
"unsafe"
)
func main() {
a := 0.1
b := 0.2
fmt.Println(a + b) // 0.30000000000000004
fmt.Printf("%d\n", unsafe.Sizeof(a)) // 8
}
没错,上述现象简单来说就是计算机计算的0.1+0.2
并不等于0.3
了,其实这个现象很常见,对别的语言来说也一样,下面通过一步步简要分析来解释这个现象
刚好在学习基础时再遇到,于是再花一点时间去拾遗下大学的基础知识,顺便记录一下(O_o)
2、浮点型数据介绍
日常程序开发并不只是用到整数,反而在多数情况下,我们用到的都是实数(有理数和无理数的集合)
实数之间的运算即浮点型运算,浮点运算不像整数运算,它的计算结果一般是不确定的。一块芯片上的浮点计算结果也许与另一块芯片上的不同
部分文字内容来源于大学时的计算机基础课程《计算机组成原理》
3、浮点数的表示形式
浮点型的科学计数法表示:N=M*rE
- M称为浮点数的尾数,
M
取小数,可正可负 - E称为浮点数的指数,也叫阶码,
E
取整数,可正可负 - r称为浮点数的基数,计算机中
r
取2
、4
、8
、16
等
浮点数在计算机中的表示,有一个IEEE
的标准,它定义了两个基本的格式:
一个是用32
比特表示单精度的浮点数,也就是我们常常说的float
另外一个是用64
比特表示双精度的浮点数,也就是我们平时说的 double
在计算机中都是用二进制存储,因此不论是32
位浮点数还是64
位浮点数,由于基数2
是固定常数,对每一个浮点数都一样,所以不必用显示的方式来表示它
对go
语言来说,分别是float32
和float64
,这两种类型的二进制表示分别如下图
那么具体是怎么转换和存储的呢?
3.1 浮点数转换为二进制
以浮点数39.29
为例
对于整数部分,直接转换为二进制,即:100111
对于小数部分,让小数一直乘2
,小于1
则用结果继续乘,大于1
则结果减1
继续乘,等于1
则结束
0.29 * 2 = 0.58 // 小于1,则继续乘
0.58 * 2 = 1.16 // 大于1,则减1继续乘
0.16 * 2 = 0.32 // 小于1,则继续乘
0.32 * 2 = 0.64 // 小于1,则继续乘
0.64 * 2 = 1.28 // 大于1,则减1继续乘
0.28 * 2 = 0.56 // 小于1,则继续乘
0.56 * 2 = 1.12 // 大于1,则减1继续乘
0.12 * 2 = 0.24 // 小于1,则继续乘
0.24 * 2 = 0.48 // 小于1,则继续乘
0.48 * 2 = 0.96 // 小于1,则继续乘
0.96 * 2 = 1.92 // 大于1,则减1继续乘
0.92 * 2 = 1.84 // 大于1,则减1继续乘
0.84 * 2 = 1.68 // 大于1,则减1继续乘
0.68 * 2 = 1.36 // 大于1,则减1继续乘
0.36 * 2 = 0.72 // 小于1,则继续乘
0.72 * 2 = 1.44 // 大于1,则减1继续乘
0.44 * 2 = 0.88 // 小于1,则继续乘
0.88 * 2 = 1.76 // 大于1,则减1继续乘
0.76 * 2 = 1.52 // 大于1,则减1继续乘
0.52 * 2 = 1.04 // 大于1,则减1继续乘
0.04 * 2 = 0.08 // 小于1,则继续乘
0.08 * 2 = 0.16 // 小于1,则继续乘
0.16 * 2 = 0.32 // 小于1,则继续乘,结果与第三行相同,一直循环
最终,由于注定不会等于1
,只能无限循环,将相乘之后的结果的整数部分拼接起来,所以0.29的二进制表示为:
01001010001111010111000......
因此,39.29
的二进制就是100111.01001010001111010111000............
3.2 科学计数法表示二进制数
简单来说,科数计数法就是把一个数字变成1.x
乘2
的多少次方。向左移了几位就是2
的几次方,如果向右移了几位就是2
的负几次方
39.29
的二进制位需要左移5
位,因此用科学计数法表示为
1.0011101001010001111010111000... * 2^5
3.3 存储科学计数法表示的二进制
- Float32,用
32
位的二进制来存储一个浮点数 - Float64,用
64
位的二进制来存储一个浮点数
以float32
位为例进行表示
-
sign:用
1
位表示浮点型的正负,0
表示正数,1
表示负数 -
exponent(指数):存储科学技术法的指数部分的值(几次方),
8
位表示的数据范围可以是0~255
,但由于指数部分可能为负数,因此exponent
有8位的表示范围是-127 ~ 128
,计算时,让指数加上127
得到的值转换为二进制存储在此处,这里是5+127=132
,转换乘二进制10000100
存储到exponent
-
fraction(小数):用
23
位来表示二进制小数的科学计数法中的小数部分
最终,39.29
在存储时的二进制为0 10000100 001110100101000111101
,后面超出的直接丢弃,这就是浮点型可能无法精确表示的原因
4、如何精确的表示浮点数
在go
中使用decimal
包可以解决浮点数精度丢失这个问题
package main
import (
"fmt"
"github.com/shopspring/decimal"
)
func main() {
c := decimal.NewFromFloat(35.2922)
fmt.Println(c.Round(1)) //保留小数点后1位自动四舍五入
fmt.Println(c.Truncate(2)) //保留小数点后2位不需要四舍五入
}
See you ~