在计算机科学中,浮点数的去规范化(下溢)问题是一个长期存在的问题,它对计算性能有着显著的影响。十年前,当Pentium 4 CPU首次出现这个问题时,就不得不面对它。然而,令惊讶的是,即使在现代硬件上,这种性能影响也没有得到缓解。StackOverflow上的一位用户报告称,在“flush to zero”模式下,Core i7的处理速度比处理去规范化数时快47倍。这是一个严重的问题。
出于好奇,在多种架构上运行了StackOverflow上的代码。以下是结果——首先,想解释一下为什么对这些结果不满意。
如果对IEEE浮点数感到困惑,需要阅读IEEE 754标准之前的浮点数历史,会发现它们更加神奇。不能真正写出可移植的代码:在某些架构上,将一个数乘以1.0会产生一个(略有不同)的数,在其他架构上,减去两个不同的数可能得到0,还有另一个架构要求在比较0之前将一个数乘以1.0... 可以在这里找到更多的例子(PDF)。
浮点数的一个特别问题是它们在0和第一个非零数之间有一个很大的“间隙”——要理解这个间隙,需要回到浮点数是如何构造的。
假设有3位十进制数字用于有效数字(SSS),一位十进制数字用于指数(E),两者都是有符号的。数是这样构造的:1.SSS * 10^E,其中1总是附加到(非零)数前面,以便在整个范围内保持十进制位数恒定(这对二进制来说更好,因为1是最大数字)。无论如何,用这种表示法,可以表示从1.999 * 10^9(最远离零)到1.000 * 10^(-9)(最接近零)的数。
现在,请注意,最小非零数和下一个数(即1.001*10^(-9))之间的距离等于0.001 * 10^(-9) == 10^(-12),这比1.000*10^(-9)和零之间的距离要小得多。
例如,在水平轴上有:
0 ----------
这个间隙的问题在于:如果从1.001*10^(-9)中减去1.000*10^(-9),不能在上述表示法(1.SSS*10^E)中表示这个差值——需要一个两位数的指数(-12)来表示。必须要么“将其冲零”,要么扩展表示法。
“冲零”意味着当减去两个不相等的数时,可能会得到零,比如1.005*10^(-9)和1.000*10^(-9)。这可能是一个问题,例如,如果使用浮点数来测量时间,程序运行得太快:帧之间的时间差在“冲零”模式下会变成零,这可能导致除以零或意外的停止运动。
去规范化数是一个特例:为它们保留一个特定的指数值,并在前面不是附加1,而是0:即它们的形式是0.SSS * 10^(-9)(注意,指数固定在最低可能的值)。这意味着这些数在接近零时会失去小数位(即精度),所以实际上午餐不是免费的,仍然必须为此付出代价(即回到前面的例子,需要考虑到一旦程序运行得更快,时间差会变得不那么精确)。
然而,午餐甚至更贵——去规范化数会带来性能损失。
在至少几年前的几种处理器上运行了StackOverflow上的代码:3.0 Ghz Xeon X5450(2007年),2.2 Ghz Athlon 64 X2(2006年?),Atom N260(2009年),Cell(2006年),PowerPC 970FX(2003年),Intel Pentium 4(2002年),Cortex A8(来自Beagleboard xM)- 2009年。
必须强调没有改变代码——也就是说,它仍然是标量的。这对上述大多数CPU来说是次优的,特别是Cortex-A8,其中VFP比Electrolux吸尘器更好,所以这个“基准测试”并不代表优化后的代码。尽管如此,不认为标量代码的性能不应该受到影响。
结果如下:
Pentium 4受到的打击最大——这是第一次遇到这个问题的架构。它在去规范化数上大约慢了103倍,但这并不新鲜。然而,令人不悦的是,多年后Intel仍然在努力使去规范化数的性能更好——Xeon是受影响第二大的处理器,在去规范化数上几乎慢了40倍,根据StackOverflow上的帖子,Core i7在这方面甚至更糟。
x86/x64 CPU在性能上名列前茅,而RISC似乎受到的影响很小。请注意,SPU始终以“冲零”模式运行,完全不受影响——或者总是受到影响,这取决于是需要性能还是数值稳定性。它被包括在这里是为了完整性,也是为了提供一些尺度。
看起来RISC架构的胜利,对吧?嗯,如果考虑绝对时间的话:
CPU Norm Denorm
Xeon 5450 0.57 sec 22.6 sec
Athlon64 0.83 sec 27.8 sec
PowerPC 970FX (G5) 1.5 sec 3.5 sec
Pentium 4 2.25 sec 230 sec (!!)
Cell SPU 3.28 sec (not applicable)
Cell PPU 3.7 sec 11.67 sec
Atom N260 7.35 sec 120 sec (!)
Cortex A8 (VFP!) 25.5 sec 44 sec