前言

在计算机系统的发展过程中,业界曾经提出过许多种实数的表达方法,比较典型的有相对于浮点数(Floating Point Number)的定点数(Fixed Point Number)。在定点数表达法中,其小数点固定地位于实数所有数字中间的某个位置。例如,货币的表达就可以采用这种表达方式,如 55.00 或者 00.55 可以用于表达具有 4 位精度,小数点后有两位的货币值。由于小数点位置固定,所以可以直接用 4 位数值来表达相应的数值。

但我们不难发现,定点数表达法的缺点就在于其形式过于僵硬,固定的小数点位置决定了固定位数的整数部分和小数部分,不利于同时表达特别大的数或者特别小的数。因此,最终绝大多数现代的计算机系统都采纳了所谓的浮点数表达法。

浮点数表达法采用了科学计数法来表达实数,即用一个有效数字。一个基数(Base)、一个指数(Exponent)以及一个表示正负的符号来表达实数。比如,666.66 用十进制科学计数法可以表达为 $6.6666×10^2$(其中,6.6666 为有效数字,10 为基数,2 为指数)。浮点数利用指数达到了浮动小数点的效果,从而可以灵活地表达更大范围的实数。

当然,对实数的浮点表示仅作如上的规定是不够的,因为同一实数的浮点表示还不是唯一的。例如,上面例子中的 666.66 可以表达为 0.66666×103、6.6666×102 或者 66.666×101 三种方式。因为这种表达的多样性,因此有必要对其加以规范化以达到统一表达的目标。规范的浮点数表达方式具有如下形式:

其中,d.dd…d 为有效数字,β 为基数,e 为指数。

有效数字中数字的个数称为精度,我们可以用 p 来表示,即可称为 p 位有效数字精度。每个数字 d 介于 0 和基数 β 之间,包括 0。更精确地说,$±d_{0}.d_{1}d_{2}…d_{p-1}×β^e$ 表示以下数:

其中,对十进制的浮点数,即基数 β 等于 10 的浮点数而言,上面的表达式非常容易理解。如 12.34,我们可以根据上面的表达式表达为:$1×10^{1}+2×10^{0}+3×10^{-1}+4×10^{-2}$,其规范浮点数表达为$1.234×10^1$。

但对二进制来说,上面的表达式同样可以简单地表达。唯一不同之处在于:二进制的 β 等于 2,而每个数字 d 只能在 0 和 1 之间取值。如二进制数 1001.101,我们可以根据上面的表达式表达为:$1×2^{3}+0×2^{2}+0×2^{1}+1×2^{0}+1×2^{-1}+0×2^{-2}+1×2^{-3}$,其规范浮点数表达为 $1.001101×2^{3}$。

现在,我们就可以这样简单地把二进制转换为十进制,如二进制数 1001.101 转换成十进制为:

由上面的等式,我们可以得出:向左移动二进制小数点一位相当于这个数除以 2,而向右移动二进制小数点一位相当于这个数乘以 2。如 101.11=3/4,而 10.111=7/8。除此之外,我们还可以得到这样一个基本规律:一个十进制小数要能用浮点数精确地表示,最后一位必须是 5(当然这是必要条件,并非充分条件)。规律推演如下面的示例所示:

我们也可以使用一段 Go 程序来验证:

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

import "fmt"

func main() {
	var f1 float32=34.6
	var f2 float32=34.5
	var f3 float32=34.0
	fmt.Printf("34.6-34.0=%f\n",f1-f3)
	fmt.Printf("34.5-34.0=%f\n",f2-f3)
	return
}

运行结果为:

1
2
34.6-34.0=0.599998
34.5-34.0=0.500000

之所以“34.6-34.0=0.599998”,产生这个误差的原因是 34.6 无法精确地表达为相应的浮点数,而只能保存为经过舍入的近似值。而这个近似值与 34.0 之间的运算自然无法产生精确的结果。

上面阐述了二进制数转换十进制数,如果你要将十进制数转换成二进制数,则需要把整数部分和小数部分分别转换。其中,整数部分除以 2,取余数;小数部分乘以 2,取整数位。如将 13.125 转换成二进制数如下:

  1. 首先转换整数部分(13),除以 2,取余数,所得结果为 1101。

  2. 其次转换小数部分(0.125),乘以 2,取整数位。转换过程如下: 0.125×2=0.25 取整数位0 0.25×2=0.5 取整数位0 0.5×2=1 取整数位1

  3. 小数部分所得结果为 001,即 13.125=1101.001,用规范浮点数表达为 $1.101001×2^{3}$。

除此之外,与浮点表示法相关联的其他两个参数是“最大允许指数”和“最小允许指数”,即 $e^{max}$ 和 $e^{min}$。由于存在 $β^{p}$ 个可能的有效数字,以及 $e^{max}$-$e^{min}$+1 个可能的指数,因此浮点数可以按 $[log_{2}(e^{max}-e^{min}+1)]+[log_{2}(β^{p})]+1$ 位编码,其中最后的 +1 用于符号位。

浮点数表示法

直到 20 世纪 80 年代(即在没有制定 IEEE 754 标准之前),业界还没有一个统一的浮点数标准。相反,很多计算机制造商根据自己的需要来设计自己的浮点数表示规则,以及浮点数的执行运算细节。另外,他们常常并不太关注运算的精确性,而把实现的速度和简易性看得比数字的精确性更重要,而这就给代码的可移植性造成了重大的障碍。

直到 1976 年,Intel 公司打算为其 8086 微处理器引进一种浮点数协处理器时,意识到作为芯片设计者的电子工程师和固体物理学家也许并不能通过数值分析来选择最合理的浮点数二进制格式。于是,他们邀请加州大学伯克利分校的 William Kahan 教授(当时最优秀的数值分析家)来为 8087 浮点处理器(FPU)设计浮点数格式。而这时,William Kahan 教授又找来两个专家协助他,于是就有了 KCS 组合(Kahn、Coonan和Stone),并共同完成了 Intel 公司的浮点数格式设计。

由于 Intel 公司的 KCS 浮点数格式完成得如此出色,以致 IEEE(Institute of Electrical and Electronics Engineers,电子电气工程师协会)决定采用一个非常接近 KCS 的方案作为 IEEE 的标准浮点格式。于是,IEEE 于 1985 年制订了二进制浮点运算标准 IEEE 754(IEEE Standard for Binary Floating-Point Arithmetic,ANSI/IEEE Std 754-1985),该标准限定指数的底为 2,并于同年被美国引用为 ANSI 标准。目前,几乎所有的计算机都支持 IEEE 754 标准,它大大地改善了科学应用程序的可移植性。

考虑到 IBM System/370 的影响,IEEE 于 1987 年推出了与底数无关的二进制浮点运算标准 IEEE 854,并于同年被美国引用为 ANSI 标准。1989 年,国际标准组织 IEC 批准 IEEE 754/854 为国际标准 IEC 559:1989。后来经修订后,标准号改为 IEC 60559。现在,几乎所有的浮点处理器完全或基本支持 IEC 60559。同时,C99 的浮点运算也支持 IEC 60559。

IEEE 浮点数标准是从逻辑上用三元组{S,E,M}来表示一个数 V 的,即 $V=(-1)×S×M×2^E$,如图1 所示。

其中:

符号位 s(Sign)决定数是正数(s=0)还是负数(s=1),而对于数值 0 的符号位解释则作为特殊情况处理。

有效数字位 M(Significand)是二进制小数,它的取值范围为 1~$2^{-ε}$,或者为 0~$1^{-ε}$。它也被称为尾数位(Mantissa)、系数位(Coefficient),甚至还被称作“小数”。

指数位 E(Exponent)是 2 的幂(可能是负数),它的作用是对浮点数加权。

浮点数格式是一种数据结构,它规定了构成浮点数的各个字段、这些字段的布局及算术解释。IEEE 754 浮点数的数据位被划分为三个段,从而对以上这些值进行编码。

1个单独的符号位 s 直接编码符号 s;
k 位的指数段 exp=$e^{k-1}…e^{1}e^{0}$,编码指数 E;
n 位的小数段 $frac=f^{n-1}…f^1f^0$,编码有效数字 M,但是被编码的值也依赖于指数域的值是否等于 0。

根据 exp 的值,被编码的值可以分为如下几种不同的情况。

格式化值

当指数段 exp 的位模式既不全为 0(即数值 0),也不全为 1(即单精度数值为 255,以单精度数为例, 8 位的指数为可以表达 0~255 的 255 个指数值;双精度数值为 2047)的时候,就属于这类情况。如图 2 所示。

我们知道,指数可以为正数,也可以为负数。为了处理负指数的情况,实际的指数值按要求需要加上一个偏置(Bias)值作为保存在指数段中的值。因此,这种情况下的指数段被解释为以偏置形式表示的有符号整数。即指数的值为:$E=e^{-Bias}$

其中,e 是无符号数,其位表示为 $e^{k-1}…e^{1}e^{0}$,而 Bias 是一个等于 $2^{k-1}-1$(单精度是 127,双精度是 1023)的偏置值。由此产生指数的取值范围是:单精度为 -126~+127,双精度为 -1022~+1023。

对小数段 frac,可解释为描述小数值 f,其中 0≤f<1,其二进制表示为 $0.f_{n-1}…f_{1}f_{0}$,也就是二进制小数点在最高有效位的左边。有效数字定义为 M=1+f。有时候,这种方式也叫作隐含的以 1 开头的表示法,因为我们可以把 M 看成一个二进制表达式为 $1.f_{n-1}…f_{1}f_{0}$ 的数字。既然我们总是能够调整指数 E,使得有效数字 M 的范围为 1≤M<2(假设没有溢出),那么这种表示方法是一种轻松获得一个额外精度位的技巧。同时,由于第一位总是等于 1,因此我们就不需要显式地表示它。拿单精度数为例,按照上面所介绍的知识,实际上可以用 23 位长的有效数字来表达 24 位的有效数字。比如,对单精度数而言,二进制的 1001.101(即十进制的 9.625)可以表达为 $1.001101×2^{3}$,所以实际保存在有效数字位中的值为:

1
00110100000000000000000

即去掉小数点左侧的 1,并用 0 在右侧补齐。

根据上面所阐述的规则,下面以实数 -9.625 为例,来看看如何将其表达为单精度的浮点数格式。具体转换步骤如下:

  1. 首先,需要将 9.625 用二进制浮点数表达出来,然后变换为相应的浮点数格式。即 9.625 的二进制为 1001.101,用规范的浮点数表达应为 1.001101×(2^3)。

  2. 其次,因为 -9.625 是负数,所以符号段为 1。而这里的指数为 3,所以指数段为 3+127=130,即二进制的 10000010。有效数字省略掉小数点左侧的 1 之后为 001101,然后在右侧用零补齐。因此所得的最终结果为:

  3. 最后,我们还可以将浮点数形式表示为十六进制的数据,如下所示:

    即最终的十六进制结果为 0xC11A0000。

特殊数值

IEEE 标准指定了以下特殊值:±0、反向规格化的数、±∞ 和 NaN(如下表所示)。这些特殊值都是使用 $e_{max+1}$ 或 $e_{min-1}$ 的指数进行编码的。

NaN:当指数段 exp 全为 1 时,小数段为非零时,结果值就被称为“NaN”(Not any Number),如图 3 所示。

一般情况下,我们将 0/0 或:$\sqrt{-1}\quad$视为导致计算终止的不可恢复错误。但是,一些示例表明在这样的情况下继续进行计算是有意义的。这时候就可以通过引入特殊值 NaN,并指定诸如 0/0 或$\sqrt{-1}\quad$之类的表达式计算来生成 NaN 而不是停止计算,从而避免此问题。下表中列出了一些可以导致 NaN 的情况。

无穷:当指数段 exp 全为 1,小数段全为 0 时,得到的值表示无穷。当 s=0 时是 +∞,或者当 s=1 时是 -∞。如图 4 所示。

无穷用于表达计算中产生的上溢问题。比如两个极大的数相乘时,尽管两个操作数本身可以保存为浮点数,但其结果可能大到无法保存为浮点数,必须进行舍入操作。根据IEEE标准,此时不能将结果舍入为可以保存的最大浮点数(因为这个数可能与实际的结果相差太远而毫无意义),而应将其舍入为无穷。对于结果为负数的情况也是如此,只不过此时会舍入为负无穷,也就是说符号域为1的无穷。

非格式化值

当指数段 exp 全为 0 时,所表示的数就是非规格化形式,如图 5 所示。

在这种情况下,指数值 E=1-Bias,而有效数字的值 M=f,也就是说它是小数段的值,不包含隐含的开头的 1。

非规格化值有两个用途:

第一,它提供了一种表示数值 0 的方法。因为规格化数必须得使有效数字 M 在范围 1≤M<2 之中,即 M≥1,因此它就不能表示 0。实际上,+0.0 的浮点表示的位模式为全 0(即符号位是 0,指数段全为 0,而小数段也全为 0),这就得到 M=f=0。令人奇怪的是,当符号位为 1,而其他段全为 0 时,就会得到值 -0.0。根据 IEEE 的浮点格式来看,值 +0.0 和 -0.0 在某些方面是不同的。

第二,它表示那些非常接近于 0.0 的数。它们提供了一种属性,称为逐渐下溢出。其中,可能的数值分布均匀地接近于 0.0。

下面的单精度浮点数就是一个非格式化的示例。

它被转换成十进制表示大约等于 1.4×10-45,实际上它就是单精度浮点数所能表达的最小非格式化数。

以此类推,格式化值和非格式化值所能表达的非负数值范围如下表所示。

标准浮点格式

IEEE 754标准准确地定义了单精度和双精度浮点格式,并为这两种基本格式分别定义了扩展格式,如下所示:

  • 单精度浮点格式(32 位)。
  • 双精度浮点格式(64 位)。
  • 扩展单精度浮点格式(≥43 位,不常用)。
  • 扩展双精度浮点格式(≥79 位,一般情况下,Intel x86 结构的计算机采用的是 80 位,而 SPARC 结构的计算机采用的是 128 位)。

其中,只有 32 位单精度浮点数是本标准强烈要求支持的,其他都是可选部分。下面就来对单精度浮点与双精度浮点的存储格式做一些简要的阐述。

单精度浮点格式

单精度浮点格式共 32 位,其中,s、exp 和 frac 段分别为 1 位、k=8 位和 n=23 位,如图 6 所示。

其中,32 位中的第 0 位存放小数段 frac 的最低有效位 LSB(least significant bit),第 22 位存放小数段 frac 的最高有效位 MSB(most significant bit);第 23 位存放指数段 exp 的最低有效位 LSB,第 30 位存放指数段 exp 的最高有效位 MSB;最高位,即第 31 位存放符号 s。例如,单精度数 8.25 的存储方式如图 7 所示。

双精度浮点格式

双精度浮点格式共 64 位,其中,s、exp 和 frac 段分别为 1 位、k=11 位和 n=52 位,如图 8 所示。

其中,frac[31:0] 存放小数段的低 32 位(即第 0 位存放整个小数段的最低有效位 LSB,第 31 位存放小数段低 32 位的最高有效位 MSB);frac[51:32] 存放小数段的高 20 位(即第 32 位存放高 20 位的最低有效位 LSB,第 51 位存放整个小数段的最高有效位 MSB);第 52 位存放指数段 exp 的最低有效位 LSB,第 62 位存放指数段 exp 的最高有效位 MSB;最高位,即第 63 位存放符号 s。

在 Intel x86 结构的计算机中,数据存放采用的是小端法(Little Endian),故较低地址的 32 位的字中存放小数段的 frac[31:0] 位。而在 SPARC 结构的计算机中,因其数据存放采用的是大端法(Big Endian),故较高地址的 32 位字中存放小数段的 frac[31:0] 位。

前面主要讨论了 IEEE 754 的单精度与双精度浮点格式,下表对浮点数的相关参数进行了总结,有兴趣的读者可以根据此表对其他浮点格式进行深入解读。

舍入误差

舍入误差是指运算得到的近似值和精确值之间的差异。大家知道,由于计算机的字长有限,因此在进行数值计算的过程中,对计算得到的中间结果数据要使用相关的舍入规则来取近似值,而这导致计算结果产生误差。

在浮点数的舍入问题上,IEEE 浮点格式定义了 4 种不同的舍入方式,如下表所示。其中,默认的舍入方法是向偶数舍入,而其他三种可用于计算上界和下界。

下表是 4 种舍入方式的应用举例。这里需要特别说明的是,向偶数舍入(向最接近的值舍入)方式会试图找到一个最接近的匹配值。因此,它将 1.4 舍入成 1,将 1.6 舍入成 2,而将 1.5 和 2.5 都舍入成 2。

或许看了上面的内容你会问:为什么要采用向偶数舍入这样的舍入策略,而不直接使用我们已经习惯的“四舍五入”呢?

其原因我们可以这样来理解:在进行舍入的时候,最后一位数字从 1 到 9,舍去的有 1、2、3、4;它正好可以和进位的 9、8、7、6 相对应,而 5 却被单独留下。如果我们采用四舍五入每次都将 5 进位的话,在进行一些大量数据的统计时,就会累积比较大的偏差。而如果采用向偶数舍入的策略,在大多数情况下,5 舍去还是进位概率是差不多的,统计时产生的偏差也就相应要小一些。

同样,针对浮点数据,向偶数舍入方式只需要简单地考虑最低有效数字是奇数还是偶数即可。例如,假设我们想将十进制数舍入到最接近的百分位。不管用哪种舍入方式,我们都将把 1.2349999 舍入到 1.23,而将 1.2350001 舍入到 1.24,因为它们不是在 1.23 和 1.24 的正中间。另一方面我们将把两个数 1.2350000 和 1.2450000 都舍入到 1.24,因为 4 是偶数。

由IEEE浮点格式定义的舍入方式可知,不论使用哪种舍入方式,都会产生舍入误差。如果在一系列运算中的一步或几步产生了舍入误差,在某些情况下,这个误差将会随着运算次数的增加而积累得很大,最终会得出没有意义的运算结果。因此,建议不要将浮点数用于精确计算。

当然,理论上增加数字位数可以减少可能会产生的舍入误差。但是,位数是有限的,在表示无限浮点数时仍然会产生误差。在用常规方法表示浮点数的情况下,这种误差是不可避免的,但是可以通过设置警戒位来减小。

除此之外,IEEE 754 还提出 5 种类型的浮点异常,即上溢、下溢、除以零、无效运算和不精确。其中,每类异常都有单独的状态标志。鉴于篇幅有限,本节就不再详细介绍。

浮点数的精度

在单精度浮点数中的二进制小数位有23个,所能表示2^23个数,那么只需要换算成在10进制下能够表示相同个数的位数,就可以得到精度了。

1
2
3
10^n = 2^23
10^n = 8388608
10^6 < 8388608 < 10^7

所以单精度浮点数的精度为6位,同理也可以得到双精度浮点数的精度为15位。

注意:精度为6位,并不是表示所有小于6的数都可以被精确存储,比如0.9。因为这个精度是由二进制的精度位数计算而来的。

所以浮点数的相等判断中,只需要判断他们的差值小于精度就可以了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <stdio.h>      /* printf */
#include <math.h>       /* fabs */

int main ()
{
  float f1 = 0.007;
  float f2 = 0.009;

  int res = ( fabs(f1-f2) < 1e-6 );
  printf ("f1 == f2 is : %s\n",res?"true":"false");
  return 0;
}

输出结果:

1
f1 == f2 is : false

单精度与双精度的区别

单精度浮点数(float)与双精度浮点数(double)的区别如下:

  1. 在内存中占有的字节数不同 单精度浮点数在机内占4个字节 双精度浮点数在机内占8个字节
  2. 有效数字位数不同 单精度浮点数有效数字8位 双精度浮点数有效数字16位
  3. 所能表示数的范围不同 单精度浮点的表示范围:-3.40E+38 ~ +3.40E+38 双精度浮点的表示范围:-1.79E+308 ~ +1.79E+308
  4. 在程序中处理速度不同 一般来说,CPU处理单精度浮点数的速度比处理双精度浮点数快。

浮点数代码示例

浮点数的表示

以0.085为例.

科学计数法能非常高效地表达极大或极小的数字,它使用小数和乘法来进行表示,下面是几个示例:

正常情况下科学计数法要求小数点左边要有一个数字,十进制时这个数字在 1 到 9 之间,二进制时只能为 1。

小数点右边的数字称为尾数,在科学计数法中所有的这些数字被称为系数,这些术语都很重要,所以请花时间去学习一下并且好好理解上面的示例。

当我们把小数点移动到首位时,指数的值会进行怎样的变化呢?如果我们将小数点移动到左侧,那么指数会是一个正数,反之会是一个负数,请观察一下上面图表中每个示例的指数值。

底数和指数需要组合起来使用,指数决定了对底数进行多少次幂的计算。在上面的第一个示例中,数字 7 与 10(底数)的 2 次方(指数)相乘得到了原始的十进制数字 700。我们将小数点向左边移动两位把 700 转换为 7.00 ,这时就会指数 +2 并且形成了科学计数法形式 7e+2 。

IEEE-754 标准不是以 10 进制的方式,而是以 2 进制来储存科学计数法。上面图表中的最后一个示例是 10 进制的数字 0.085 用 2 进制的科学计数法来表示,让我们学习一下这是如何计算出来的。

我们需要用 2 的若干次方除以 10 进制的数字(0.085)来获得一个以 1 开头的分数,“以1开头的分数”是什么意思呢?在示例中我们需要一个看上去像是系数的值 1 + .36。IEEE-754 标准中需要系数以 “1.” 开头,这让我们必须储存尾数部分并且需要额外的比特位来表示精度。

下面我们将采用一种暴力的方法,你会看到最终会得到代表着 0.085 并且以 1 开头的分数:

-4 这个指数让我们获得了所需的以 1 开头的分数,现在我们已经具备了将 10 进制数字 0.085 储存为 IEEE-754 格式的所有条件。

让我们来看看在 IEEE-754 格式中的比特位是如何排列的。

这些比特位可以分为三个部分,先是一个用来标记符号的比特位,然后是表示指数和分数部分的比特位。我们会将尾数作为二进制分数形式储存在分数比特位中。

当我们把 0.085 存储为单精度(32位数字)时,IEEE-754的位模式看上去就像这样:

最左边的符号位决定了这个数的正负,如果把符号位设置为1,那么这个数就是负数。

接下来的 8 位代表着指数。在我们的示例中,十进制数字 0.085 被转换成以2为底的科学计数法格式 1.36 * 2-4,因此指数为 -4。为了能够表示负数,指数位会有一个偏移值,当数字是 32 位时这个偏移值为 127。我们需要找到一个数,这个数减去偏移值能得到 -4,在我们的示例中这个数为 123。如果你注意一下指数的位模式就会发现这个二进制表示的是数字 123。

剩下的 23 位为分数位,为了得到出分数位的位模式,我们需要对二进制的分数位进行计算和求和,直到求出尾数或者最为接近尾数的值,因为我们假定整数部分一直为 “1.”,所以只要储存尾数部分。

查看下面的图表你就会明白二进制分数位是如何被计算出来的,从左到右的每一位都表示不同的分数值。

我们需要设置正确的分数位来累加得到尾数,或者是足够接近尾数的值,这也是为什么有时我们会丢失一些精度。

010 1110 0001 0100 0111 1011 = (0.36)

你会看到当这12个比特位排列好之后,我们就得到了0.36这个值,以及后面还带有一些额外的分数。让我们总结一下现在所知道的IEEE-754格式:

  1. 任何10进制的数字都会被储存为基于科学计数法的格式。
  2. 基于2进制的科学计数法必须遵循以1开头的分数格式。
  3. 整个格式被分为截然不同的三部分。
  4. 符号位决定了数字的正负。
  5. 指数位表示一个减去偏移量的值。
  6. 分数位表示使用二进制分数累加得到的尾数。

让我们来验证一下对于 IEEE-754 格式的分析是否正确。我们应该可以把 0.85 这个数字储存为位模式,并且它每一部分的值和我们之前看到的应该一致。

接下来的代码储存了以二进制 IEEE-754 表示的数字 0.085:

 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
41
42
43
44
45
46
47
48
package main

import (
    "fmt"
    "math"
)

func main() {
    var number float32 = 0.085

    fmt.Printf("Starting Number: %f\n\n", number)

    // Float32bits returns the IEEE 754 binary representation
    bits := math.Float32bits(number)

    binary := fmt.Sprintf("%.32b", bits)

    fmt.Printf("Bit Pattern: %s | %s %s | %s %s %s %s %s %s\n\n",
        binary[0:1],
        binary[1:5], binary[5:9],
        binary[9:12], binary[12:16], binary[16:20],
        binary[20:24], binary[24:28], binary[28:32])

    bias := 127
    sign := bits & (1 << 31)
    exponentRaw := int(bits >> 23)
    exponent := exponentRaw - bias

    var mantissa float64
    for index, bit := range binary[9:32] {
        if bit == 49 {
            position := index + 1
            bitValue := math.Pow(2, float64(position))
            fractional := 1 / bitValue

            mantissa = mantissa + fractional
        }
    }

    value := (1 + mantissa) * math.Pow(2, float64(exponent))

    fmt.Printf("Sign: %d Exponent: %d (%d) Mantissa: %f Value: %f\n\n",
        sign,
        exponentRaw,
        exponent,
        mantissa,
        value)
}

当我们运行程序时会显示下面的输出:

1
2
3
4
5
Starting Number: 0.085000

Bit Pattern: 0 | 0111 1011 | 010 1110 0001 0100 0111 1011

Sign: 0 Exponent: 123 (-4) Mantissa: 0.360000 Value: 0.085000

如果你将输出中的位模式和之前的示例对比一下,那么你会发现它们是一致的,所以我们之前所学习的 IEEE-754 都是正确的。

识别浮点数

假设 uf 是一个无符号的 64 位整型,但包含的内容却是符合 IEEE-754 标准的二进制浮点数,你怎么去区分 uf 是表示整型还是浮点型呢?

下面是一个能检测整型是否被储存为 IEEE-754 格式的函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
func IsInt(bits uint32, bias int) {
    exponent := int(bits >> 23) - bias - 23
    coefficient := (bits & ((1 << 23) - 1)) | (1 << 23)
    intTest := (coefficient & (1 << uint32(-exponent) - 1))

    fmt.Printf("\nExponent: %d Coefficient: %d IntTest: %d\n",
        exponent,
        coefficient,
        intTest)

    if exponent < -23 {
        fmt.Printf("NOT INTEGER\n")
        return
    }

    if exponent < 0 && intTest != 0 {
        fmt.Printf("NOT INTEGER\n")
        return
    }

    fmt.Printf("INTEGER\n")
}

那么这个函数是如何进行检测的呢?让我们先将指数小于 -23 作为首要条件进行测试,如果用 1 作为我们的测试值,那么指数和偏移值都是 127,这意味着我们用指数减去偏移值会得到 0。

1
2
3
4
5
6
7
8
Starting Number: 1.000000

Bit Pattern: 0 | 0111 1111 | 000 0000 0000 0000 0000 0000

Sign: 0 Exponent: 127 (0) Mantissa: 0.000000 Value: 1.000000

Exponent: -23 Coefficient: 8388608 IntTest: 0
INTEGER

在测试函数中减去了一个额外的 23,它表示 IEEE-754 格式中指数的比特位开始的位置,这也就是为什么你会看到测试函数中会出现 -23 这个指数值。

在第二个测试中必须要用到减法,所以任何小于 -23 的值必定小于 1,因此不是一个整型。

让我们用一个整型值来理解第二个测试是如何工作的,这次我们将要在代码中把这个值设置为 234523,然后再一次运行程序。

1
2
3
4
5
6
7
8
9
Starting Number: 234523.000000

Bit Pattern: 0 | 1001 0000 | 110 0101 0000 0110 1100 0000

Sign: 0 Exponent: 144 (17) Mantissa: 0.789268 Value: 234523.000000


Exponent: -6 Coefficient: 15009472 IntTest: 0
INTEGER

第二个测试通过两个条件来识别这个数字是否为整型,这需要用到按位运算,下面提供了一个演示函数,让我们看一下其中的算法:

1
2
3
    exponent := int(bits >> 23) - bias - 23
    coefficient := (bits & ((1 << 23) - 1)) | (1 << 23)
    intTest := coefficient & ((1 << uint32(-exponent)) - 1)

系数的计算需要向尾数部分增加1, 因此我们有了基于2进制的系数值。

当我们查看系数计算的第一部分时,会看到下面的位模式:

1
2
3
4
5
coefficient := (bits & ((1 << 23) - 1)) | (1 << 23)

Bits:                   01001000011001010000011011000000
(1 << 23) - 1:          00000000011111111111111111111111
bits & ((1 << 23) - 1): 00000000011001010000011011000000

第一部分的系数计算中从 IEEE-754 位模式中移除了符号位和指数位。

第二部分的计算中会把 “1 +” 加入到位模式中。(注:看图就会明白,就是分数位最高位的前一位进行了或操作变为1)

1
2
3
4
5
coefficient := (bits & ((1 << 23) - 1)) | (1 << 23)

bits & ((1 << 23) - 1): 00000000011001010000011011000000
(1 << 23):              00000000100000000000000000000000
coefficient:            00000000111001010000011011000000

到了这时系数就已经确定了,我们用这个 intTest 函数来计算一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
exponent := int(bits >> 23) - bias - 23
intTest := (coefficient & ((1 << uint32(-exponent)) - 1))

exponent:                     (144 - 127 - 23) = -6
1 << uint32(-exponent):       000000
(1 << uint32(-exponent)) - 1: 111111

coefficient:                 00000000111001010000011011000000
1 << uint32(-exponent)) - 1: 00000000000000000000000000111111
intTest:                     00000000000000000000000000000000

我们通过测试函数计算出的指数值通常用来 确定 下一步中用来比较系数值。在这个例子中指数值为 -6,这个值是使用所储存的指数值(144)减去偏移值(127),再减去指数的开始位置(23)所计算出来的。-6 表示的位模式是 6 个 1(1‘s),最后的操作是将这个 6 个比特位对系数的最右边 6 位进行位与计算,最终就得到了 intTest 的值。

第二个测试函数是在 initTest 值不为 0 的情况下寻找小于零 (0) 的指数值,表明这个数字中储存的不是整型。在值为 234523 的这个示例中,指数小数零 (0) 但是 intTest 的值大于零 (0),说明这是一个整型。

整体代码如下:

  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
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
package main

import (
	"fmt"
	"math"
)

func main() {
	var number float32 = 234523

	fmt.Printf("Starting Number: %f\n\n", number)

	// Float32bits returns the IEEE 754 binary representation
	bits := math.Float32bits(number)

	binary := fmt.Sprintf("%.32b", bits)

	fmt.Printf("Bit Pattern: %s | %s %s | %s %s %s %s %s %s\n\n",
		binary[0:1],
		binary[1:5], binary[5:9],
		binary[9:12], binary[12:16], binary[16:20],
		binary[20:24], binary[24:28], binary[28:32])

	bias := 127
	sign := bits & (1 << 31)
	exponentRaw := int(bits >> 23)
	exponent := exponentRaw - bias

	var mantissa float64
	for index, bit := range binary[9:32] {
		if bit == 49 {
			position := index + 1
			bitValue := math.Pow(2, float64(position))
			fractional := 1 / bitValue

			mantissa = mantissa + fractional
		}
	}

	value := (1 + mantissa) * math.Pow(2, float64(exponent))

	fmt.Printf("Sign: %d Exponent: %d (%d) Mantissa: %f Value: %f\n\n",
		sign,
		exponentRaw,
		exponent,
		mantissa,
		value)

	IsInt(bits, bias)
}

func IsInt(bits uint32, bias int) {
	exponent := int(bits>>23) - bias - 23
	coefficient := (bits & ((1 << 23) - 1)) | (1 << 23)
	intTest := coefficient & ((1 << uint32(-exponent)) - 1)

	ShowBits(bits, bias, exponent)

	fmt.Printf("\nExp: %d Frac: %d IntTest: %d\n",
		exponent,
		coefficient,
		intTest)

	if exponent < -23 {
		fmt.Printf("NOT INTEGER\n")
		return
	}

	if exponent < 0 && intTest != 0 {
		fmt.Printf("NOT INTEGER\n")
		return
	}

	fmt.Printf("INTEGER\n")
}

func ShowBits(bits uint32, bias int, exponent int) {
	value := (1 << 23) - 1
	value2 := (bits & ((1 << 23) - 1))
	value3 := (1 << 23)
	coefficient := (bits & ((1 << 23) - 1)) | (1 << 23)

	fmt.Printf("Bits:\t\t\t%.32b\n", bits)
	fmt.Printf("(1 << 23) - 1:\t\t%.32b\n", value)
	fmt.Printf("bits & ((1 << 23) - 1):\t\t%.32b\n\n", value2)

	fmt.Printf("bits & ((1 << 23) - 1):\t\t%.32b\n", value2)
	fmt.Printf("(1 << 23):\t\t\t%.32b\n", value3)
	fmt.Printf("coefficient:\t\t\t%.32b\n\n", coefficient)

	value5 := 1 << uint32(-exponent)
	value6 := (1 << uint32(-exponent)) - 1
	inTest := (coefficient & ((1 << uint32(-exponent)) - 1))

	fmt.Printf("1 << uint32(-exponent):\t\t%.32b\n", value5)
	fmt.Printf("(1 << uint32(-exponent)) - 1:\t%.32b\n\n", value6)

	fmt.Printf("coefficient:\t\t\t%.32b\n", coefficient)
	fmt.Printf("(1 << uint32(-exponent)) - 1:\t%.32b\n", value6)
	fmt.Printf("intTest:\t\t\t%.32b\n", inTest)
}

参考: http://c.biancheng.net/view/314.html https://studygolang.com/articles/14407