现代语言为了达到编译一次、到处运行的目的,纷纷采用虚拟机的方式,将目标语言锁定为虚拟机支持的中间语言。这样不需要知道最终程序运行的软硬件系统,达到所谓的平台无关性,最典型的例子就是Java和C#了。平台无关性想法很好,但是实现起来却并不容易,充满了挑战。其中之一的挑战就是浮点数运算。
浮点数一般是指float和double,其中float是32位,double是64位。我们先看下面这个简单的语句:
double w = x * y / z;
要想在所有的机器上运行这条语句,得到相同的w值是很困难的,这很CPU的设计很有关系。现在很多的Intel处理器,在进行浮点运算是,使用的是80位的寄存器,而不是64位的。这些CPU是这样处理上述这条语句的:
步骤1. 计算x * y ,得到中间结果w0,将w0存入一个80位的寄存器;
步骤2. 计算w0/z,得到中间结果w1,将w1存入一个80位的寄存器;
步骤3. 将80位的w1裁剪成60位的w。
很明显,相比全部使用64位寄存器的CPU,采用80位寄存器的Intel CPU更精确,因为w0更精确。同时,这也说明了这条语句在不同的CPU下运行,得到的结果可能不一样,这也就违背了平台无关性这条原则。
当然,为了达到平台无关性,虚拟机可以在上述步骤1和步骤2之间,增加下面这条指令:
步骤1.5 将80位的w0裁剪成60位的w0;
这样做有两个缺点:
1. 计算效率降低了,因为多运行了一条指令;
2. 计算结果可能不正确,出现指数溢出的情况,因为x * y的结果有可能超过double的范围了。
那么,虚拟机到底是否需要生成步骤1.5的指令呢?Java很聪明,它把这个决定权给了开发者。默认情况下,Java是不会生成步骤1.5的指令的。然而,Java提供了一个关键字strictfp,可以让Java生成这条多余的指令。例如,下面的函数multiply能保证,它里面的浮点运算在所有的平台上得到一致的结果。
public static strictfp double multiply(double x, double y)
当然,其实我们不用太在意了strictfp关键字,因为用到它的时候很少,绝大多数情况下不用strictfp,只要知道有这回事情就可以了。