30

一两年前,编写数字代码以避免使用乘法和除法而是使用加法和减法是值得的。一个很好的例子是使用前向差分来评估多项式曲线,而不是直接计算多项式。

仍然是这种情况,还是现代计算机体系结构已经发展到 *,/ 不再比 +,- 慢很多倍的程度?

具体来说,我对在具有广泛板载浮点硬件的现代典型 x86 芯片上运行的编译 C/C++ 代码感兴趣,而不是试图在软件中进行 FP 的小型微型计算机。我意识到流水线和其他架构增强排除了特定的循环计数,但我仍然想获得一个有用的直觉。

4

6 回答 6

26

它还取决于指令组合。您的处理器将随时有多个计算单元待命,如果所有这些单元一直都被填满,您将获得最大的吞吐量。因此,执行 mul 的循环与执行循环或添加一样快 - 但如果表达式变得更复杂,则同样不成立。

例如,采取这个循环:

for(int j=0;j<NUMITER;j++) {
  for(int i=1;i<NUMEL;i++) {
    bla += 2.1 + arr1[i] + arr2[i] + arr3[i] + arr4[i] ;
  }
}

对于 NUMITER=10^7, NUMEL=10^2,两个数组都初始化为小的正数(NaN 慢得多),在 64 位 proc 上使用双精度需要 6.0 秒。如果我用

bla += 2.1 * arr1[i] + arr2[i] + arr3[i] * arr4[i] ;

只需要 1.7 秒……所以由于我们“过度”添加,所以 muls 基本上是免费的;并且减少添加有所帮助。它变得更加混乱:

bla += 2.1 + arr1[i] * arr2[i] + arr3[i] * arr4[i] ;

-- 相同的 mul/add 分布,但现在常数被添加而不是相乘 -- 需要 3.7 秒。您的处理器可能经过优化,可以更有效地执行典型的数值计算;所以点积之类的 muls 和缩放的和几乎一样好;添加常量并不常见,所以速度较慢......

bla += someval + arr1[i] * arr2[i] + arr3[i] * arr4[i] ; /*someval == 2.1*/

再次需要 1.7 秒。

bla += someval + arr1[i] + arr2[i] + arr3[i] + arr4[i] ; /*someval == 2.1*/

(与初始循环相同,但没有昂贵的常量添加:2.1 秒)

bla += someval * arr1[i] * arr2[i] * arr3[i] * arr4[i] ; /*someval == 2.1*/

(主要是 muls,但增加一个:1.9 秒)

所以,基本上;很难说哪个更快,但如果你想避免瓶颈,更重要的是要有一个理智的组合,避免 NaN 或 INF,避免添加常量。无论您做什么,请确保您测试并测试各种编译器设置,因为通常小的更改可以产生影响。

还有一些案例:

bla *= someval; // someval very near 1.0; takes 2.1 seconds
bla *= arr1[i] ;// arr1[i] all very near 1.0; takes 66(!) seconds
bla += someval + arr1[i] * arr2[i] + arr3[i] * arr4[i] ; // 1.6 seconds
bla += someval + arr1[i] * arr2[i] + arr3[i] * arr4[i] ; //32-bit mode, 2.2 seconds
bla += someval + arr1[i] * arr2[i] + arr3[i] * arr4[i] ; //32-bit mode, floats 2.2 seconds
bla += someval * arr1[i]* arr2[i];// 0.9 in x64, 1.6 in x86
bla += someval * arr1[i];// 0.55 in x64, 0.8 in x86
bla += arr1[i] * arr2[i];// 0.8 in x64, 0.8 in x86, 0.95 in CLR+x64, 0.8 in CLR+x86
于 2009-07-18T18:22:47.973 回答
19

理论上,信息在这里:

英特尔® 64 和 IA-32 架构优化参考手册,附录 C 指令延迟和吞吐量

对于他们列出的每个处理器,FMUL 的延迟都非常接近 FADD 或 FDIV。在一些较旧的处理器上,FDIV 比这慢 2-3 倍,而在较新的处理器上,它与 FMUL 相同。

注意事项:

  1. 我链接的文档实际上说你不能在现实生活中依赖这些数字,因为如果它是正确的,处理器会做它希望让事情变得更快的事情。

  2. 您的编译器很有可能会决定使用具有可用浮点乘法/除法的许多较新指令集之一。

  3. 这是一个复杂的文档,仅供编译器编写者阅读,我可能弄错了。就像我不清楚为什么某些 CPU 完全缺少 FDIV 延迟数。

于 2009-07-18T17:22:22.213 回答
9

回答这个问题的最佳方法是实际编写您需要执行的处理的基准/配置文件。在可能的情况下,应该使用经验而不是理论。尤其是当它很容易达到时。

如果你已经知道你需要做的数学的不同实现,你可以编写一些不同的数学代码转换,看看你的性能峰值在哪里。这将允许处理器/编译器生成不同的执行流来填充处理器管道并为您的答案提供具体答案。

如果您对 DIV/MUL/ADD/SUB 类型指令的性能特别感兴趣,您甚至可以加入一些内联汇编来具体控制执行这些指令的哪些变体。但是,您需要确保让多个执行单元保持忙碌状态,以便更好地了解系统的性能。

此外,您还可以通过在处理器上运行相同的程序来比较处理器的多个变体的性能,并且还可以让您考虑主板的差异。

编辑:

+- 的基本架构是相同的。因此,它们在逻辑上需要相同的时间来计算。* 另一方面,需要多个层,通常由“全加器”构成以完成单个操作。这表明,虽然每个周期都可以向管道发出 *,但它的延迟将比加/减电路更高。fp / 操作通常使用一种近似方法来实现,该方法随着时间的推移迭代地收敛到正确的答案。这些类型的近似值通常通过乘法来实现。因此,对于浮点,您通常可以假设除法将花费更长的时间,因为将乘法(它本身已经是一个大电路)“展开”到多个乘法器电路的管道中是不切实际的。

于 2009-07-18T16:35:13.947 回答
2

我找不到明确的参考,但广泛的实验告诉我,现在的浮点乘法与加法和减法的速度几乎相同,而除法则不是(但也不会慢“很多倍”)。您只能通过运行自己的实验来获得所需的直觉——记住提前生成随机数(数百万个),在开始计时之前阅读它们,并使用 CPU 的性能计数器(没有其他进程运行,如尽你所能阻止他们)进行准确测量!

于 2009-07-18T02:15:45.307 回答
1

* / vs + - 的速度差异取决于您的处理器架构。一般来说,尤其是 x86,现代处理器的速度差异已经变小了。* 应该接近 +,如果有疑问:只是实验。如果您在处理大量 FP 操作时遇到了非常棘手的问题,还可以考虑使用作为矢量处理器的 GPU(GeForce,...)。

于 2009-07-18T02:29:05.067 回答
-1

乘法和加法之间的时间差异可能很小。另一方面,由于其递归性质,除法仍然比乘法慢得多。在进行浮点运算而不是使用 fpu 时,应考虑现代 x86 架构上的 sse 指令。尽管一个好的 C/C++ 编译器应该为您提供使用 sse 而不是 fpu 的选项。

于 2009-07-18T02:21:47.747 回答