我正在对我们核心数学代码进行性能分析,使用Intel Core Duo处理器。在尝试使用不同的计算平方根的方法时,我注意到了一个奇怪的现象:使用SSE标量运算,先取倒数平方根再乘以它比使用本机sqrt操作码更快!
我正在使用类似以下循环进行测试:
inline float TestSqrtFunction( float in );
void TestFunc()
{
#define ARRAYSIZE 4096
#define NUMITERS 16386
float flIn[ ARRAYSIZE ]; // filled with random numbers ( 0 .. 2^22 )
float flOut [ ARRAYSIZE ]; // filled with 0 to force fetch into L1 cache
cyclecounter.Start();
for ( int i = 0 ; i < NUMITERS ; ++i )
for ( int j = 0 ; j < ARRAYSIZE ; ++j )
{
flOut[j] = TestSqrtFunction( flIn[j] );
// unrolling this loop makes no difference -- I tested it.
}
cyclecounter.Stop();
printf( "%d loops over %d floats took %.3f milliseconds",
NUMITERS, ARRAYSIZE, cyclecounter.Milliseconds() );
}
我已经尝试了几种不同的TestSqrtFunction实现方式,但是得到了一些让我感到很困惑的计时结果。其中最糟糕的是使用本地的sqrt()函数并让“智能”编译器进行优化。使用x87 FPU每秒24纳秒/浮点数,这个速度非常差:
inline float TestSqrtFunction( float in )
{ return sqrt(in); }
接下来我尝试的是使用内置函数来强制编译器使用SSE的标量平方根指令:
inline void SSESqrt( float * restrict pOut, float * restrict pIn )
{
_mm_store_ss( pOut, _mm_sqrt_ss( _mm_load_ss( pIn ) ) );
// compiles to movss, sqrtss, movss
}
这个方法更好,每个浮点数只需要11.9ns。我还试过卡马克疯狂的牛顿拉弗森逼近技术,速度比硬件还要快,只需要4.3ns/float,但误差为2的10次方之一(对于我的目的来说太大了)。
最棒的是,当我使用SSE操作进行倒数平方根运算,然后使用乘法得到平方根(x * 1/√x = √x)。虽然这需要两个相依的操作,但它是迄今为止速度最快的解决方案,每个浮点数只需1.24ns,且准确精度可达2的-14次方:
inline void SSESqrt_Recip_Times_X( float * restrict pOut, float * restrict pIn )
{
__m128 in = _mm_load_ss( pIn );
_mm_store_ss( pOut, _mm_mul_ss( in, _mm_rsqrt_ss( in ) ) );
// compiles to movss, movaps, rsqrtss, mulss, movss
}
我的问题基本上是,为什么?SSE内置的硬件平方根操作码比通过合成其他两个数学运算慢?
我确定这主要是由于操作本身的成本,因为我已经验证了:
- 所有数据都适合缓存,并且访问是顺序的
- 函数被内联
- 展开循环没有任何影响
- 编译器标志设置为完全优化(汇编代码很好,我检查过)
(编辑:stephentyrone正确指出,长数字字符串上的操作应使用矢量化SIMD打包操作,如rsqrtps
,但此处的数组数据结构仅用于测试目的:我真正尝试衡量的是无法进行矢量化的代码中的标量性能。)
inline float SSESqrt(float restrict fIn)
的意思是对输入参数进行平方根计算,并返回结果。但这种实现方式不好,因为如果CPU将浮点数写入堆栈,然后立即读取它们,就很容易引起加载阻塞存储。特别是将向量寄存器中的值转移到浮点寄存器以返回值,则更加不可取。此外,SSE intrinsic所表示的底层机器指令也需要使用地址操作数。 - Crashworks