pow(NAN) is very slow

pow(NAN) is very slow

本文关键字:very slow is NAN pow      更新时间:2023-10-16

对于NaN值,pow()灾难性性能的原因是什么?据我所知,如果浮点运算是用SSE而不是x87 FPU完成的,那么nan应该不会对性能产生影响。

这似乎对基本运算是正确的,但对pow()不是。我把一个双数的乘法和除法比作平方然后取平方根。如果我用g++ -lrt编译下面的代码,我得到以下结果:

multTime(3.14159): 20.1328ms
multTime(nan): 244.173ms
powTime(3.14159): 92.0235ms
powTime(nan): 1322.33ms

正如预期的那样,涉及NaN的计算需要相当长的时间。但是,使用g++ -lrt -msse2 -mfpmath=sse编译会导致以下时间:

multTime(3.14159): 22.0213ms
multTime(nan): 13.066ms
powTime(3.14159): 97.7823ms
powTime(nan): 1211.27ms

NaN的乘法/除法现在快多了(实际上比实数快),但是平方和平方根仍然需要很长时间。

测试代码(使用gcc 4.1.2在32位OpenSuSE 10.2在VMWare下编译,CPU为Core i7-2620M)

#include <iostream>
#include <sys/time.h>
#include <cmath>
void multTime( double d )
{
   struct timespec startTime, endTime;
   double durationNanoseconds;
   clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &startTime);
   for(int i=0; i<1000000; i++)
   {
      d = 2*d;
      d = 0.5*d;
   }
   clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &endTime);
   durationNanoseconds = 1e9*(endTime.tv_sec - startTime.tv_sec) + (endTime.tv_nsec - startTime.tv_nsec);
   std::cout << "multTime(" << d << "): " << durationNanoseconds/1e6 << "ms" << std::endl;
}
void powTime( double d )
{
   struct timespec startTime, endTime;
   double durationNanoseconds;
   clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &startTime);
   for(int i=0; i<1000000; i++)
   {
      d = pow(d,2);
      d = pow(d,0.5);
   }
   clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &endTime);
   durationNanoseconds = 1e9*(endTime.tv_sec - startTime.tv_sec) + (endTime.tv_nsec - startTime.tv_nsec);
   std::cout << "powTime(" << d << "): " << durationNanoseconds/1e6 << "ms" << std::endl;
}
int main()
{
   multTime(3.14159);
   multTime(NAN);
   powTime(3.14159);
   powTime(NAN);
}
编辑:

不幸的是,我对这个主题的了解非常有限,但我猜glibc pow()从来没有在32位系统上使用SSE,而是在sysdeps/i386/fpu/e_pow.S中使用一些汇编。在最近的glibc版本中有一个函数__ieee754_pow_sse2,但它在sysdeps/x86_64/fpu/multiarch/e_pow.c中,因此可能只适用于x64。但是,所有这些在这里可能都无关紧要,因为pow()也是gcc内置函数。要解决这个问题,请参见Z玻色子的答案。

"如果浮点运算是用SSE而不是x87 FPU完成的,nan应该不会对性能产生影响。"

我不确定这是否符合你引用的资源。在任何情况下,pow都是一个C库函数。即使在x87上,它也不是作为指令实现的。所以这里有两个独立的问题- SSE如何处理NaN值,以及pow函数实现如何处理NaN值。

如果pow函数的实现对特殊值(如+/-InfNaN)使用不同的路径,您可能期望基数或指数的NaN值能够快速返回值。另一方面,实现可能不会将此作为单独的情况处理,而只是依赖于浮点操作将中间结果传播为NaN值。

从"Sandy Bridge"开始,许多与异常相关的性能惩罚被减少或消除。但并非全部,正如作者描述的mulps的惩罚。因此,可以合理地预期,并非所有涉及NaNs的算术运算都是"快速"的。有些体系结构甚至可以在不同的上下文中恢复为微码来处理NaNs

你的数学库太旧了。要么找到另一个更好地使用NAN实现pow的数学库,要么实现如下修复:

inline double pow_fix(double x, double y) 
{
    if(x!=x) return x;
    if(y!=y) return y;
    return pow(x,y);
}

g++ -O3 -msse2 -mfpmath=sse foo.cpp编译

如果要平方或取平方根,请使用d*dsqrt(d)pow(d,2)pow(d,0.5)会更慢,而且可能不太准确,除非你的编译器基于第二个常数2和0.5对它们进行优化;请注意,这样的优化可能并不总是适用于pow(d,0.5),因为如果d是负零,它返回0.0,而sqrt(d)返回-0.0。

对于那些做计时的人,请确保你测试的是相同的东西

对于像pow()这样的复杂函数,NaN触发慢的方式有很多。可能是对NaN的操作很慢,也可能是pow()实现检查了它可以有效处理的各种特殊值,而NaN值未能通过所有这些测试,从而导致采用更昂贵的路径。

最近的pow()实现可能包括额外的检查以更有效地处理NaN,但这始终是一种权衡——为了加速NaN处理而让pow()更慢地处理"正常"情况将是一种耻辱。

我的博客文章只适用于单个指令,而不是像pow()这样的复杂函数