几年前,在20世纪90年代初,我构建了图形包,该包基于不动点算法优化计算,并使用牛顿近似方法为sqrt和log近似的cos、sin和缩放方程的预计算表。这些先进的技术似乎已经成为图形和内置数学处理器的一部分。大约5年前,我上了一堂数值分析课,学习了一些古老的技术。我已经编码了将近30年,很少看到那些旧的定点优化在使用,即使是在为世界级粒子加速器实验开发GPGPU应用程序之后。在整个软件行业的任何地方,定点方法仍然有用吗?或者这些知识的有用性现在已经一去不复返了吗?
固定点在不支持任何类型的十进制的平台上都很有用;例如,我为PIC16F系列微控制器实现了一种24位定点类型(稍后将详细介绍我为什么选择定点)。
然而,几乎每个现代CPU都支持微码或硬件级别的浮点,因此不太需要定点。
固定点号在其所能代表的范围内是有限的——考虑64位(32.32)固定点与64位浮点:64位固定点号的十进制分辨率为1/(232),而浮点号的十进制分辨力为至1/(253);定点数可以表示高达231的值,而浮点数可以表示高达223。如果您需要更多,大多数现代CPU都支持80位浮点值。
当然,浮点的最大缺点是在极端情况下精度有限——例如,在固定点中,它需要更少的比特来表示90000000000000000000000000000000000000000000000000000002。当然,有了浮点运算,你可以获得更好的精度来平均使用十进制算术,我还没有看到一个应用程序的十进制算术像上面的例子一样极端,但也不会溢出等效的定点大小。
我为PIC16F实现定点库而不是使用现有的浮点库的原因是代码大小,而不是速度:16F88有384字节的可用RAM,可容纳4095条指令。为了添加两个预定义宽度的不动点,我在代码中使用进位内联了整数加法(不动点无论如何都不会移动);为了将两个不动点相乘,我使用了一个简单的带有扩展的32位不动点的移位和加法函数,尽管这不是最快的乘法方法,以节省更多的代码。
所以,当我只需要一两个基本的算术运算时,我可以在不耗尽所有程序存储的情况下添加它们。相比之下,该平台上免费提供的浮点库约占设备总存储量的60%。相比之下,软件浮点库大多只是一些算术运算的包装,根据我的经验,它们大多要么全有要么全无,所以因为只需要一半的函数而将代码大小减半并不能很好地工作。
不过,由于固定点的表示范围有限,它在速度上通常没有太大优势:用15位精度表示1.7E+/-308需要多少位,与64位双精度相同?如果我的计算是正确的,你需要大约2020比特。我敢打赌那场比赛的表现不会那么好。
30年前,当硬件浮点相对罕见时,与基于软件的浮点相比,非常专用的定点(甚至缩放整数)算法可以在性能上提供显著的提高,但前提是允许的值范围可以用比例整数算术有效地表示(最初的Doom在没有协处理器的情况下使用了这种方法,比如1992年在我的486sx-25上——在一个运行频率为4.0GHz的超频超线程酷睿i7上用一个拥有1000多个独立浮点计算单元的GeForce卡输入这种方法,不知何故,这似乎是错误的,尽管我不确定是486还是i7…)
浮点更通用,因为它可以表示的值范围很广,而且它在CPU和GPU上的硬件中实现,在各个方面都优于定点,除非你真的需要超过80位的浮点精度,而代价是巨大的定点大小和非常慢的代码。
好吧,我编码了20年,我的经验是使用固定点有3个主要原因:
-
没有可用的FPU
定点仍然适用于DSP、MCU、FPGA和芯片设计。此外,没有定点核心单元,任何浮点单元都无法工作,因此所有bigdecimal库都必须使用定点。。。图形卡也经常使用固定点(标准化的设备坐标)。
-
FPU精度不足
如果你进行天文计算,你很快就会达到极限,需要处理它们。例如,简单的牛顿/达朗贝尔积分或大气射线跟踪在大尺度和低粒度上很快达到精度障碍。我通常使用浮点加倍数组来弥补这一点。对于输入/输出范围已知的情况,固定点通常是更好的选择。参见击中FPU屏障的一些示例:
- 有可能在大小和质量方面进行真实的n体太阳系模拟吗
- 射线与椭球面交会精度的提高
-
速度
在过去,FPU确实很慢(尤其是在x86架构上),因为它使用了接口和api。每个FPU指令都会生成一个中断,更不用说操作数和结果传输过程了。。。因此CPU ALU中的少量位移位操作通常更快。
如今,这已不再是事实,ALU和FPU的速度相当。例如,这里挖掘CPU/FPU操作的测量(在小型Win32 C++应用程序中):
fcpu(0) = 3.194877 GHz // tested on first core of AMD-A8-5500 APU 3.2GHz Win7 x64 bit CPU 32bit integer aritmetics: add = 387.465 MIPS sub = 376.333 MIPS mul = 386.926 MIPS div = 245.571 MIPS mod = 243.869 MIPS FPU 32bit float aritmetics: add = 377.332 MFLOPS sub = 385.444 MFLOPS mul = 383.854 MFLOPS div = 367.520 MFLOPS FPU 64bit double aritmetics: add = 385.038 MFLOPS sub = 261.488 MFLOPS mul = 353.601 MFLOPS div = 309.282 MFLOPS
这些值随时间而变化,但相比之下,不同的数据类型几乎相同。就在几年前,由于数据传输量是原来的两倍,速度变慢了一倍。但在其他平台上,速度差异可能仍然有效。