程序员

首页 » 常识 » 问答 » 每个C语言程序员都应该明白,计算机究竟是
TUhjnbcbe - 2023/8/4 20:37:00

浮点型在内存中的存储分布方式因机器平台而异,完全理解所有机器平台中的浮点型存储无疑是一件相当麻烦的事。幸运的是,大多机器平台都遵守IEEE-标准,很可能读者和我使用的平台正是使用的IEEE-标准。

计算机如何存储浮点数

IEEE-是如何存储浮点数的?

IEEE-浮点(32位)或双精度(64位)有三个部分(在IEEE-下也有类似的96位扩展精度格式):符号位,表示数字是正的还是负的;指数位;以及指定实际数字的尾数位。以C语言中的单精度浮点数为例,下面是某位浮点数的位布局:

某位浮点数的位布局

该浮点数的值等于尾数乘以2^x。读者应该注意,上图是二进制分数,因此0.1表示1/2。为了方便理解,我们可以将其与十进制的小数对应起来:十进制的0.1等于1*10^-1,所以二进制的0.1等于1*2^-1,也即1/2。

“尾数+指数”模式存储浮点数可能有一点问题,例如:2x10^-1=0.2x10^0=0.02x10^1,以此类推。同样一个数字可能有多种“尾数+指数”的表示方法,而同时兼顾多种表示方法势必会造成巨大的浪费(也可能使在硬件中实现数学操作变得困难和缓慢)。

同时兼顾多种表示方法势必会造成巨大的

所以,“尾数+指数”的存储模式需要一个统一的标准。事实上,IEEE-确实已经有标准了:假设给定一个二进制的浮点数,那么除非这个数是0,否则总有某个位是1。将小数点移到第一个1之后,调整指数位,这样一来,“尾数+指数”的唯一存储方式就固定下来了,也即“1.mx2^n”形式。

既然小数点前总是1,那么上述标准下的“尾数+指数”的存储模式甚至都不需要再花费空间存储小数点前的1.

但是如果数字是零呢?IEEEStandardsCommittee通过将零作为一种特殊情况来解决这一问题:如果数字的每一位都为零,那么数字就被认为是零。

现在读者可能又有疑问了,因为1.0=1.0×2^0,上述存储模式不存储小数点前的1,也即尾数和指数部分都为0,而“如果数字的每一位都为零,那么数字就被认为是零”,这样看来,1.0似乎是没有办法存储的。

1.0似乎是没有办法存储的

当然可以存储1.0。单精度浮点数的指数部分是“shift-”编码的,也即实际的指数等于eeeeee减去,所以1.0的表示方法实际上是1.0×2^。同样的道理,最小值本应该是2^-,按照“shift-”编码指数部分,也即2^0,可是这样又变成“指数部分和尾数部分都为零”了,因此在该标准下的最小值,实际上的写法是2^1,也即2^-。

在我看来,为了表示0和1,舍弃最小值(2^-)是非常可取的做法。

零不是唯一的“特殊情况”。对于正无穷大和负无穷大,非数字(NaN),以及没有数学意义的结果(例如,非实数,或无穷大乘以零之类的计算结果)也有表示:如果指数的每一位都等于1,那么这个数字是无穷大,如果指数的每一位都等于1,并且尾数位也都等于1,那么这个数字就是NaN。符号位仍然区分+/-inf和+/-nan。

现在,读者应该明白IEEE-浮点数的表示方法了,下面是几个数字的表示方法:

几个数字的表示方法

作为程序员,了解浮点表示的某些特性是很重要的,下标列出了单精度和双精度IEEE浮点数的示例值:

单精度和双精度IEEE浮点数的示例值

注意,本文中的所有数字都假定为单精度浮点数;上面包含双精度浮点数用于参考和比较。

在C语言程序开发中,数值的处理是一门值得深究的科学。本文不可能将复杂的数值算法以及相关的C语言程序开发经验一一列出。事实上,讨论如何以理想的数值精度进行计算,就和讨论如何编写最快的C语言程序,如何设计一款优秀的软件一样,主要取决于程序员本身的综合素质。

鉴于此,这里将尝试介绍一些基础的,我认为每个C语言程序员都应该知道的内容。

相等

首先,我们应该明白C语言程序开发中的两个浮点数何时相等。可能读者并不觉得难,因为似乎C语言中的==运算符就能判断两个浮点数是否完全相等。

然而实际上,C语言中的==运算符是逐位比较两个操作数的,而两个浮点数的精度总是有限的,在这种场景下,==运算符的实际使用意义就没有那么大了。

==运算符的实际使用意义就没有那么大

读者应该已经明白,计算机存储浮点数时,很有可能是需要舍弃一些位的(如果该浮点数过长),如果CPU或者相应的程序没有按照预期四舍五入,那么使用==运算符判断两个浮点数是否相等可能会失败。

例如,标准C语言函数库三角函数cos()的实现其实只是一种多项式近似,也就是说,我们并不能指望cos(π/2)结果的每一个位都为零。在C语言程序开发中,我们甚至不能准确的表示π。

看到这里,读者应该思考“相等到底是什么意思呢?”,对于大多数情况来说,两个数“相等”意味着这两个数“足够接近”。本着这种精神,在实际的C语言程序开发中,程序员通常定义一个很小的值模拟“足够接近”,并以此判断两个浮点数是否“足够接近到相等”,例如:

判断两个浮点数是否“足够接近到相等”

宏flt_equals(a,b)正是通过判断a和b的距离是否小于EPSILON(10的-7次方),来断定a和b是否可以被认为“相等”的。这样的近似模拟技术有时候是有用的,有时候却可能导致错误结果,读者应该自行判断它是否符合自己的程序。

在本例中,EPSILON可以看作是一种用于说明程序精度的标尺。应该明白,衡量精度的应该是有效数字,纠结EPSILON的具体大小并无意义,下面是一个例子。

假设在某段C语言程序中有两个数字1.25e-20和2.25e-20,它俩的差值是1e-20,远小于EPSILON,但是显然它俩并不相等。但是如果这两个数字是1.e-20和1.e-20,那么就可以认为它们是相等的。也就是说,两个数字距离足够接近时,我们还需要

1
查看完整版本: 每个C语言程序员都应该明白,计算机究竟是