这两天翻看一本C 语言书的时候,发现上面有一段这样写到
例:将同一实型数分别赋值给单精度实型和双精度实型,然后打印输出。
#include <stdio.h>
main()
{
float a;
double b;
a = 123456.789e4;
b = 123456.789e4;
printf(“%f/n%f/n”,a,b);
}
运行结果如下:
1234567936.000000
1234567890.000000
为什么同一个实型数据赋值给float 型变量和double 型变量之后,输出的结果会有所不同呢?是因为将一个实型常量赋值给float 型变量与赋值给double 型变量,它们所接受的有效数字位是不同的。
这一段的说法是正确的,但实在是太模糊了!为什么一个输出的结果会比原来的大?为什么不是比原来的小?这之间到底有没有什么内存的根本性原因还是随机发生的?为什么会出现这样的情况?上面都没有对此进行解释。上面的解释是一种最普通的解释,甚至说它只是说出了现象,而并没有很深刻的解释原因,这不免让人读后觉得非常不过瘾!
书中还有下面一段:
(1)两个整数相除的结果仍为整数,舍去小数部份的值。例如,6/4 与6.0/4 运算的结果值是不同的,6/4 的值为整数1,而6.0/4 的值为实型数1.5。这是因为当其中一个操作数为实数时,则整数与实数运算的结果为double 型。非常遗憾的说,“整数与实数运算的结果为double 型”,这样的表述是不精确的,不论从实际程序的反汇编结果,还是从对CPU 硬件结构的分析,这样的说法都非常值得推敲。然而在很多C 语言的教程上我们却总是常常看见这样的语句:“所有涉及实数的运算都会先转换成double,然后再运算”。然而实际又是否是这样的呢? 关于浮点数运算这一部份,绝大多数的C 教材没有过多的涉及,这也使得我们在使用C 语言的时候,会产生很多疑问。
先来看看下面一段程序:
/* -------------a.c------------------ */
#include <stdio.h>
double f(int x)
{
return 1.0 / x ;
}
void main()
{
double a , b;
int i ;
a = f(10) ;
b = f(10) ;
i = a == b ;
printf( "%d/n" , i ) ;
}
这段程序使用gcc –O2 a.c 编译后,运行它的输出结果是0,也就是说a 不等于b,为
什么?
再看看下面一段,几乎同上面一模一样的程序:
/*---------------- b.c ----------------------*/
#include <stdio.h>
double f(int x)
{
return 1.0 / x ;
}
void main()
{
double a , b , c;
int i ;
a = f(10) ;
b = f(10) ;
c = f(10) ;
i = a == b ;
printf( "%d/n" , i ) ;
}
同样使用gcc –O2 b.c 编译,而这段程序输出的结果却是1,也就是说a 等于b,为什
么? 国内几乎没有一本C 语言书(至少我还没看见),解释了这个问题,在C 语言对浮点数的处理方面,国内的C 语言书几乎都是浅尝即止,蜻蜓点水,而国外的有些书对此就有很详尽的描述,上面的例子就是来源于国外的一本书《Computer Systems A Programmer’s Perspective》(本文参考文献2,以下简称《CSAPP》),这本书对C 语言及CPU 处理浮点数描写的非常细致深入,国内很多书籍明显不足的地方,就在于对于某些细节我们是乎并没有某种深入的精神,没有一定要弄个水落石出的气度,这也注定了我们很少出版一些Bible 级的著作。一本书如果值得长期保留,能成为Bible,那么我认为它必须把某一细节描述的非常清楚,以至于在读了此书之后,再也不需要阅读其它的书籍,就能对此细节了如指掌。《CSAPP》这本书的确非常经典,遗憾的是此书好像目前还没有电子版,因此我打算以此书为基础(一些例子及描述就来自此书),再加上自己看过的一些其它资料,以及自己对此问题的理解与分析,详细谈一下C 语言及Intel CPU 对浮点数的处理,以期望在此方面,能对不清楚这部分内容的学弟学妹们有些许帮助。要无障碍的阅读此文,你需要对C 语言及汇编有所了解,本文的所有实验,均基于Linux 完成,硬件基于Intel IA32 CPU,因此,如果你想从此文中了解更多,你最好能熟练使Linux 下的gcc 及objdump 命令行工具(非常遗憾的是,现在少有C 语言教材会对此进行讲述),
另外,你还需要对堆栈操作有所了解,这在任何一部讲解数据结构的书上都会提到。由于自身知识及能力有限,如果书中有描述不当的地方或错误,请你与我联系,我也会在http://jaminwm.2003y.net对所有问题进行跟踪及反馈。
一、Intel CPU 浮点运算单元的逻辑结构
在很久以前,由于CPU 工艺的限制,无法在一个单一芯片内集成一个高性能的浮点运算器,因此,Intel 还专门开发了所谓的协处理器配合主处理器完成高性能的浮点运算,比如80386 的协处理器就是80387,后来由于集成电路工艺的发展,人们已经能够将在一个芯片内集成更多的逻辑功能单元,因此,在80486DX 的时候,Intel 就在80486DX 这个芯片内集成了很强大的浮点处理单元。下面,我们就来看看,被集成到主处理器内部之后,这个浮点处理单元的逻辑结构,这是理解Intel CPU 对浮点数处理机制的前提条件。
79
R0
R1
R2
R3
R4
R5
R6
R7
0 15
控制寄存器
状态寄存器
标志寄存器
(图1 Intel CPU 浮点处理单元逻辑结构图)
上图就是Intel IA32 架构CPU 浮点处理单元的逻辑结构图,从图中我们可以看出它总
0
8 个数据寄存器
0 47
最近一次指令指针
最近一次操作数指针
0 10
操作码寄存器
共有8 个数据寄存器,每个80 位(10B);一个控制寄存器(Control Register),一个状态寄
存器(Status Register),一个标志寄存器(Tag Register),每个16 位(2B);还有一个最近
一次指令指针(Last Instruction Pointer),及一个最近一次操作数指针(Last Operand Pointer),
每个48 位(6B);以及一个操作码寄存器(Opcode Register)。
状态寄存器用处与常见的主CPU 的程序状态字差不多,用来标记运算是否溢出,是否
产生错误等,最主要的一点是它还记录了8 个数据寄存器的栈顶位置(这点在下面将会有详
细描述)。
控制寄存器中最重要的就是它指定了这个浮点处理单元的舍入的方式(后面将会对此详
细描述)及精度(24 位,53 位,64 位)。Intel CPU 浮点处理器的默认精度是64 位,也称为
Double Extended Precision(中文也许会译为:双扩展精度,但这种专有名词,不译更好,译
了反而感觉更别扭)。而24 位,与53 位的精度,是为了支持IEEE 所定义的浮点标准(IEEE
754 标准),也就是C 语言中的float 与double。
标志寄存器指出了8 个寄存器中每个寄存器的状态,比如它们是否为空,是否可用,是
否为零,是否是特殊值(比如NaN:Not a Number)等。
最后一次指令指针寄存器与最后一次数据指针寄存器用来存放最后一条浮点指令(非控
制用指令)及所用到的操作数在内存中的位置。由于包括16 位的段选择符及32 位的内存偏
移地址,因此,这两个寄存器都是48 位(这涉及到Intel IA32 架构下的内存地址访问方法,
如果对此不清楚的,可以不用太在意,只需知道它们指明了一个内存地址就行,如果很想弄