IEEE 754(IEEE二进制浮点数算术标准)是 20 世纪 80 年代以来使用最广泛的浮点数运算标准,被许多 CPU 与浮点运算器所采用。
在上世纪六、七十年代,各家计算机公司的各个型号计算机,有着千差万别的浮点数表示,这给数据交换、计算机协同工作造成了极大不便。
在 1980 年,英特尔公司就推出了单片的 8087 浮点数协处理器,其浮点数表示法及定义的运算具有足够的合理性、先进性,被 IEEE 采用作为浮点数的标准,于 1985 年发布。因此第一个标准就是 IEEE 754-1985。
科学计数法
先来说说科学计数法。科学计数法大家肯定都不陌生,它是一种数字的表示法,最早由阿基米德提出。在科学记数法中,一个数被写成一个实数 a 与一个 10 的 n 次幂的积的形式:
1 | value = a * 10ⁿ |
其中:
- n 必须是一个整数
- 1 ≤ |a| < 10(如果 |a| 是一个小于 1 的小数,或大于等于 10 的数,都可以通过改变 n 来表示)
- a 是一个实数,称为有效数或尾数(mantissa)
科学计数法的优点是:当需要表示非常大或非常小的数时,如果用一般的方法,要将一个数的所有位数都写出来,很难直接明确知道它的大小,还会浪费很多空间。如果使用科学记数法来表示,一个数的数量级、精确度和数值都会非常直观。
比如,0.00000000000000000000000167262158 的科学计数法表示为 1.67262158 * 10⁻²⁴;1898130000000000000000000000 的科学计数法表示为 1.89813 * 10²⁷。
为什么要先说科学计数法呢?因为 IEEE 754 标准的表示方法几乎就是二进制的科学计数法。
表示
IEEE 754 规定了四种表示浮点数值的方式:单精确度(32位)、双精确度(64位)、延伸单精确度(43比特以上,很少使用)与延伸双精确度(79比特以上,通常以80位实现)。
本文主要的研究对象是单精确度浮点数(32位)。其他精度,比如双精确度浮点数(64位),其和单精确度其实是大同小异的。
二进制浮点数的表示如下:
1 | value = sign * exponent * fraction |
其中
- sign(sign bit):符号位
- exponent(exponent bias):指数偏移值
- fraction:分数值
二进制浮点数的组成如下图:
对照上图,单精确度浮点数(32位)表示如下:
双精确度浮点数(64位)表示如下:
一些概念
下面解释一些概念。
(1)符号位(sign bit)
符号位为 0 表示正数,符号位为 1 表示负数。
(2)指数的实际值
一个数二进制科学计数法表示下的指数的值。
例如 2¹⁷,那么指数的实际值就是 17。
(3)指数偏移值(exponent bias)
指的是根据 IEEE 754 标准对浮点数二进制表示中指数部分编码的值:
1 | exponent bias = 指数的实际值 + 某个固定值 |
IEEE 754 标准规定:该固定值为 2ᵉ⁻¹ - 1,e 为存储指数的比特的长度。
例如,单精度浮点数的指数域是 8 个比特,那么固定偏移值为 2⁸⁻¹ - 1 = 128 - 1 = 127。如果指数实际值为17₁₀,那么在单精度浮点数中的指数域编码值为 17₁₀ + 127₁₀ = 144₁₀。
为什么要用这种形式来表示浮点数的指数呢?
因为这样可以用长度为 e 个比特的无符号整数来表示所有的指数取值,这使得两个浮点数的指数大小的比较也更为容易,实际上可以直接按照字典次序比较两个浮点表示的大小。
这种移码表示的指数部分,中文称作阶码。
normal number
normal number:规约形式的浮点数。
“规约”指的是用唯一确定的浮点形式去表示一个值。
如果浮点数中指数部分的编码值范围为 (0, 2ᵉ - 2](即指数位不全为 0,也不全为 1),且在科学表示法的表示方式下,fraction 部分的最高有效位是 1(fraction 部分的范围是 [1, 2)),那么这个浮点数被称为规约形式的浮点数。
因此,规约形式的单精度浮点数的指数部分编码值范围为 (0, 254],这也可以反推出规约形式的单精度浮点数对应的指数部分实际取值是 [-126, 127]。
为什么没有 -127 和 128 呢?因为 -127 和 128 被用作特殊值处理,见下面“非规约形式的浮点数”和“特殊值”。
由于这种表示下的尾数有一位隐含的二进制有效数字,为了与二进制科学计数法的尾数相区别,IEEE 754 称之为有效数(significant)。
subnormal number
subnormal number:非规约形式的浮点数。
为什么需要非规约形式的浮点数?
由规约形式的浮点数部分的内容,我们可以推算出,规约形式的单精度浮点数的取值范围为:
1 | ±尾数 * 2 ^ 指数 |
转换为十进制后的大致范围是:
1 | [-3.4 * 10³⁸, -1.18 * 10⁻³⁸] ∪ [1.18 * 10⁻³⁸, 3.4 * 10³⁸] |
这里就有个问题,使用规约形式的浮点数我们无法表示非常接近 0 的极小的数(即 (-1 * 2⁻¹²⁶, 1 * 2⁻¹²⁶) 之间的数)!非规约形式的浮点数的出现就将解决这个问题。
什么是非规约形式的浮点数?
如果浮点数的指数部分的编码值是 0(即指数位全为 0),且 fraction 部分非零(fraction 部分的最高有效位是 0,fraction 部分的范围是 (0, 1)),那么这个浮点数被称为非规约形式的浮点数。
IEEE 754 标准规定:非规约形式的浮点数的指数偏移值比规约形式的浮点数的指数偏移值小 1。
例如,最小的规约形式的单精度浮点数的指数部分编码值为 1,指数的实际值为 -126;而非规约的单精度浮点数的指数域编码值为 0,对应的指数实际值也是 -126 而不是 -127,这是规定。至于为什么这么规定,下面会说到。
非规约形式的浮点数怎么表示非常接近 0 的数?
准确来说,这个问题应该是非规约形式的浮点数怎么表示 (-1 * 2⁻¹²⁶, 1 * 2⁻¹²⁶) 之间的数?
同样,我们可以推算出非规约形式的单精度浮点数的取值范围为:
1 | ±尾数 * 2 ^ 指数 |
仔细看下,非规约形式的单精度浮点数的区间范围和规约形式的单精度浮点数的区间范围正好构成了 (-2 * 2¹²⁷, 0) ∪ (0, 1 * 2 * 2¹²⁷)。
(是不是中间少个 0 很难受,不要急,0 是作为特殊值存在的,见下面的“特殊值”)
什么是渐进式下溢出(gradual underflow)?
根据前面的内容我们可以知道:
非规约形式的单精度浮点数的最大值为 0 00000000 1111111111111111111111,其实就是 0.1111111111111111111111 * 2⁻¹²⁶
规约形式的单精度浮点数的最小值(大于 0 的区间中)为 0 00000001 0000000000000000000000,其实就是 1.0000000000000000000000 * 2⁻¹²⁶
可以看到,两者之间几乎实现了平滑的过度,非规约形式的单精度浮点数的最大值非常紧密的连接上了规约形式的单精度浮点数的最小值(大于 0 的区间中,小于 0 的区间也同理)。
这种特性在 IEEE 754 中就称为*渐进式下溢出(gradual underflow)*。
理解了这个特性,也就解释了前面的两个疑问:
- 为什么规定非规约形式浮点数的尾数前隐藏的整数部分是 0,而规约形式浮点数尾数前隐藏的整数部分是 1 ?
- 为什么规定非规约形式浮点数的指数实际值的计算公式是 1 - bias,而规约形式浮点数的指数实际值计算公式是 指数部分的值 - bias ?
就是因为有这些规定才能实现渐进式下溢出这种特性。
还有一个概念与之对应,就是*突然式下溢出(abrupt underflow)*。顾名思义,不解释了。
non-number
non-number:特殊值。
有三个特殊值:
- 如果指数是 0 并且 fraction 的小数部分是 0,这个数是 ±0(和符号位相关)
- 如果指数是 2ᵉ - 1 并且 fraction 的小数部分是 0,这个数是 ±∞(和符号位相关)
- 什么是 ±∞ 呢?比如对于规约形式的单精度浮点数来说,存储的值超过了它的范围,比如存储的值是 4 * 10³⁸
- 如果指数是 2ᵉ - 1 并且 fraction 的小数部分非 0,这个数表示为非数(NaN)
- 什么是 ±∞ 呢?比如要存储的值是 √-1
总结
以上规则,可总结如下:
形式 | 指数 | 小数部分 |
---|---|---|
零 | 全为 0 | 全为 0 |
非规约形式 | 全为 0 | 大于 0 小于 1 |
规约形式 | 1 到 2ᵉ - 2(不全为 0 也不全为 1) | 大于等于 1 小于 2 |
无穷 | 2ᵉ - 1(全为 1) | 全为 0 |
NaN | 2ᵉ - 1(全为 1) | 非 0 |
凑字数
网上有几篇文章我觉得写得挺不错,我也参考了部分内容,这里共享出来链接以供大家阅读:
https://www.bilibili.com/read/cv4546492
https://www.jianshu.com/u/9d6b8017232d
鉴于这篇已经写了这么多字,就先打住,下篇会用具体示例再来演示一下。