golang 与其他很多语言(C、C++、Python…)一样,使用了IEEE-754标准存储浮点数。
32位的浮点数 与 64位浮点数的差异
精度 | 符号位 | 指数位 | 小数位 | 偏移量 |
---|---|---|---|---|
32 bit | 1[31] | 8[30-23] | 23[22-0] | 127 |
64 bit | 1[63] | 11[62-52] | 52[51-0] | 1023 |
首先我们来看十进制浮点数是如何转换为二进制的
十进制浮点数 | 二进制浮点数 | 计算方式 | 二进制科学计数法 |
---|---|---|---|
3.5 | 11.1 | 3.5 = 2 1 + 2 0 + 2 − 1 3.5 = 2^{1} + 2^{0} + 2^{-1} 3.5=21+20+2−1 | 1.11 × 2 1 1.11 \times 2^{1} 1.11×21 |
0.5 | 0.1 | 0.5 = 2 − 1 0.5 = 2^{-1} 0.5=2−1 | 1.0 × 2 − 1 1.0 \times 2^{-1} 1.0×2−1 |
0.25 | 0.01 | 0.25 = 2 − 2 0.25 = 2^{-2} 0.25=2−2 | 1.0 × 2 − 2 1.0 \times 2^{-2} 1.0×2−2 |
0.125 | 0.001 | $ 0.125 = 2^{-3} $ | 1.0 × 2 − 3 1.0 \times 2^{-3} 1.0×2−3 |
0.0625 | 0.0001 | 0.0625 = 2 − 4 0.0625 = 2^{-4} 0.0625=2−4 | 1.0 × 2 − 4 1.0 \times 2^{-4} 1.0×2−4 |
0.8 | 0.11001100… | 0.8 = 2 − 1 + 2 − 2 + 2 − 5 + 2 − 6 + . . . 0.8 = 2^{-1} + 2^{-2} + 2^{-5} + 2^{-6} + ... 0.8=2−1+2−2+2−5+2−6+... | 1.1001100... × 2 − 1 1.1001100... \times 2^{-1} 1.1001100...×2−1 |
从上面我们可以观察到,对于任何数来说,表示成二进制科学计数法后,都成以转换成 1.xxx(尾数) * 2 的 n 次方(指数)。
另外如上图中的十进制小数0.8,表示成二进制后变成了以1100循环的无限循环小数。这便是浮点数有精度问题的根源之一,在代码中声明的小数0.8,计算机底层其实是无法精确存储那个无限循环的二进制数的。只能存储一个0舍1入后的近似值
同样的对于负数来说可以表示为 -1.xxx(尾数) * 2 的 n 次方(指数), 所以内存中要存储这个小数可以拆成三个部分存储:
我们如何快速将一个十进制浮点数转成二进制浮点数呢? 下面我们以3.66为例进行演示
// 先将数拆成整数位和小数位
// 整数位转换方法同整数
3 --> 11
.66 --> .
// 用剩余的小数乘以2, 然后提取结果的整数位, 直到结果为1或超出存储上限
0.66 * 2 = 1.32 --> 1
0.32 * 2 = 0.64 --> 0
0.64 * 2 = 1.28 --> 1
0.28 * 2 = 0.56 --> 0
0.56 * 2 = 1.12 --> 1
0.12 * 2 = 0.24 --> 0
0.24 * 2 = 0.48 --> 0
0.48 * 2 = 0.96 --> 0
0.96 * 2 = 1.92 --> 1
0.92 * 2 = 1.84 --> 1
.
.
.
// 所以3.66转换为二进制浮点数为
11.1010100011...
正如上述过程所示, 浮点数的整数部分按照整数的方式直接转成二进制, 小数部分乘以2后提取整数部分剩余部分继续乘2并提取整数部分, 重复执行此步骤知道结果为1或达到我们能表示的最大精度
以0.8为例 1.1001100... × 2 − 1 1.1001100... \times 2^{-1} 1.1001100...×2−1, 其具体存储方式如下
// golang 打印浮点数的二进制表示方法
package main
import (
"fmt"
"math"
)
func main() {
fmt.Println(fmt.Sprintf("%032b", math.Float32bits(0.8)))
}
符号位 | 指数位 | 小数位(尾数) |
---|---|---|
0 | 0111 1110 | 100 1100 1100 1100 1100 1101 |
符号位: 0表示正数, 1表示负数
指数位: 01111110 对应的值是126, 那么为什么本应该是-1的值却是126呢?
尾数: 也就是二进制科学计数法小数点右侧的部分, 32位浮点数尾数最长23bit
我们思考一个问题, 以上述我们所说的规约形式的浮点数能表示的大于0的最小浮点数是多少呢? 是 2 − 127 2^{-127} 2−127吗?
那我们尝试以上节讲的内容将 2 − 127 2^{-127} 2−127表示出来试一下
符号位 | 指数位 | 小数位 |
---|---|---|
0 | − 127 + 127 = 0 -127 + 127 = 0 −127+127=0 | 1.0 ∗ 2 − 127 1.0 * 2^{-127} 1.0∗2−127 |
0 | 0000 0000 | 000 0000 0000 0000 0000 0000 |
我们发现所有位都成为了0, 而在浮点数中0的表示方法就是所有位都是0
实际在IEEE 754标准规定中最小的规约形式的单精度浮点数的指数部分编码值为1, 对于32位浮点数而言指数的实际值为-126, 也就是说规约形式的浮点数能表示的大于0的最小浮点数是 2 − 126 2^{-126} 2−126, 将指数部分编码值为0的部分用于表示非规约形式的浮点数
以下摘录自百度百科对非规约形式的浮点数的介绍
如果浮点数的指数部分的编码值是0,分数部分非零,那么这个浮点数将被称为非规约形式的浮点数。一般是某个数字相当接近零时才会使用非规约型式来表示。 IEEE 754标准规定:非规约形式的浮点数的指数偏移值比规约形式的浮点数的指数偏移值小1。例如,最小的规约形式的单精度浮点数的指数部分编码值为1,指数的实际值为-126;而非规约的单精度浮点数的指数域编码值为0,对应的指数实际值也是-126而不是-127。实际上非规约形式的浮点数仍然是有效可以使用的,只是它们的绝对值已经小于所有的规约浮点数的绝对值;即所有的非规约浮点数比规约浮点数更接近0。规约浮点数的尾数大于等于1且小于2,而非规约浮点数的尾数小于1且大于0。
除了规约浮点数,IEEE754-1985标准采用非规约浮点数,用来解决填补绝对值意义下最小规格数与零的距离。(举例说,正数下,最大的非规格数等于最小的规格数。而一个浮点数编码中,如果exponent=0,且尾数部分不为零,那么就按照非规约浮点数来解析)非规约浮点数源于70年代末IEEE浮点数标准化专业技术委员会酝酿浮点数二进制标准时,Intel公司对渐进式下溢出(gradual underflow)的力荐。当时十分流行的DECVAX机的浮点数表示采用了突然式下溢出(abrupt underflow)。如果没有渐进式下溢出,那么0与绝对值最小的浮点数之间的距离(gap)将大于相邻的小浮点数之间的距离。例如单精度浮点数的绝对值最小的规约浮点数是$1.0 \times 2^{-126} $,它与绝对值次小的规约浮点数之间的距离为 2 − 126 × 2 − 23 = 2 − 149 2^{-126} \times 2^{-23} = 2^{-149} 2−126×2−23=2−149。如果不采用渐进式下溢出,那么绝对值最小的规约浮点数与0的距离是相邻的小浮点数之间距离的 2 23 2^{23} 223倍!可以说是非常突然的下溢出到0。这种情况的一种糟糕后果是:两个不等的小浮点数X与Y相减,结果将是0.训练有素的数值分析人员可能会适应这种限制情况,但对于普通的程序员就很容易陷入错误了。采用了渐进式下溢出后将不会出现这种情况。例如对于单精度浮点数,指数部分实际最小值是(-126),对应的尾数部分从 1.1111...11 1.1111...11 1.1111...11 , 1.1111...10 1.1111...10 1.1111...10一直到 0.0000...10 , 0.0000...01 , 0.0000...00 0.0000...10, 0.0000...01, 0.0000...00 0.0000...10,0.0000...01,0.0000...00相邻两小浮点数之间的距离(gap)都是 2 − 126 × 2 − 23 = 2 − 149 2^{-126} \times 2^{-23} = 2^{-149} 2−126×2−23=2−149;而与0最近的浮点数(即最小的非规约数)也是 2 − 126 × 2 − 23 = 2 − 149 2^{-126} \times 2^{-23} = 2^{-149} 2−126×2−23=2−149。
这里有三个特殊值需要指出
符号位 | 指数位 | 小数位 | 实际值 |
---|---|---|---|
*1 | 0 | 0 | ± 0 \pm 0 ±0 |
*1 | 2 e − 1 2^{e} - 1 2e−1 | 0 | ± ∞ \pm \infty ±∞ |
*1 | 2 e − 1 2^{e} - 1 2e−1 | 非0 | NaN |
正如我们本小节开头的例子 2 − 127 2^{-127} 2−127不再以 1.0 ∗ 2 − 127 1.0 * 2^{-127} 1.0∗2−127表示而是表示为 0.1 ∗ 2 − 126 0.1 * 2^{-126} 0.1∗2−126, 同理 2 − 130 2^{-130} 2−130表示为 0.0001 ∗ 2 − 126 0.0001 * 2^{-126} 0.0001∗2−126, 因为首位必然是0所以存储的时候依然只存储尾数部分, 比如 2 − 130 2^{-130} 2−130将存储为
符号位 | 指数位 | 小数位(尾数) |
---|---|---|
0 | 0000 0000 | 000 1000 0000 0000 0000 0000 |
类别 | 正负号 | 实际指数 | 有偏移指数 | 指数域 | 尾数域 | 数值 |
---|---|---|---|---|---|---|
0 | 0 | -127 | 0 | 0000 0000 | 000 0000 0000 0000 0000 0000 | 0.0 |
-0 | 1 | -127 | 0 | 0000 0000 | 000 0000 0000 0000 0000 0000 | −0.0 |
1 | 0 | 0 | 127 | 0111 1111 | 000 0000 0000 0000 0000 0000 | 1.0 |
-1 | 1 | 0 | 127 | 0111 1111 | 000 0000 0000 0000 0000 0000 | −1.0 |
最小的非规约数 | *1 | -127 | 0 | 0000 0000 | 000 0000 0000 0000 0000 0001 | ± 2 − 126 × 2 − 23 ≈ ± 1.4 × e − 45 \pm 2^{-126} \times 2^{-23} \approx \pm 1.4 \times e^{-45} ±2−126×2−23≈±1.4×e−45 |
中间大小的非规约数 | *1 | -127 | 0 | 0000 0000 | 100 0000 0000 0000 0000 0000 | ± 2 − 127 ≈ ± 5.88 × e − 39 \pm 2^{-127} \approx \pm 5.88 \times e^{-39} ±2−127≈±5.88×e−39 |
最大的非规约数 | *1 | -127 | 0 | 0000 0000 | 111 1111 1111 1111 1111 1111 | ± ( 2 − 126 − 2 − 149 ) ≈ ± 1.18 × e − 38 \pm(2^{-126} - 2^{-149}) \approx \pm 1.18 \times e^{-38} ±(2−126−2−149)≈±1.18×e−38 |
最小的规约数 | *1 | -126 | 1 | 0000 0001 | 000 0000 0000 0000 0000 0000 | ± 2 − 126 ≈ ± 1.18 × e − 38 \pm 2^{-126} \approx \pm 1.18 \times e^{-38} ±2−126≈±1.18×e−38 |
最大的规约数 | *1 | 127 | 254 | 1111 1110 | 111 1111 1111 1111 1111 1111 | ± ( 2 128 − 2 105 ) ≈ ± 3.4 × e 38 \pm(2^{128} - 2^{105}) \approx \pm 3.4 \times e^{38} ±(2128−2105)≈±3.4×e38 |
正无穷 | 0 | 128 | 255 | 1111 1111 | 000 0000 0000 0000 0000 0000 | + ∞ + \infty +∞ |
负无穷 | 1 | 128 | 255 | 1111 1111 | 000 0000 0000 0000 0000 0000 | − ∞ - \infty −∞ |
NaN | *1 | 128 | 255 | 1111 1111 | non zero | NaN |