我一直在Intel Core Duo上进行一些核心数学分析,在查看各种平方根方法时,我注意到了一些奇怪的事情:使用SSE标量运算,倒数平方根乘以它会更快获取sqrt,而不是使用本机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()函数并让“智能”编译器“优化”。在24ns / float的情况下,使用x87 FPU确实很糟糕:
inline float TestSqrtFunction( float in )
{ return sqrt(in); }
我尝试的下一件事是使用内部函数强制编译器使用SSE的标量sqrt操作码:
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 / float。我还尝试了Carmack的古怪的Newton-Raphson逼近技术,其运行速度甚至比硬件还要好,为4.3ns / float,尽管误差为2比10(对于我而言,这太过分了)。
当我尝试SSE op求倒数平方根,然后使用乘法运算得到平方根时,就产生了混淆。即使需要两次相关操作,它还是迄今为止最快的解决方案,速度为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 ) { float fOut; _mm_store_ss( &fOut, _mm_sqrt_ss( _mm_load_ss( &fIn ) ) ); return fOut; }
。但这是一个坏主意,因为如果CPU将浮点数写入堆栈然后立即读回它们-会很容易导致加载命中存储停顿-特别是从向量寄存器到浮点寄存器中以获取返回值是个坏消息。此外,SSE内在函数表示的基础机器操作码无论如何都采用地址操作数。
eax
)之间移动数据都是非常糟糕的,而xmm0和堆栈之间的往返由于英特尔的存储转发,后退不是。您可以自己安排时间以确保查看。通常,查看潜在LHS的最简单方法是查看发出的程序集,并查看寄存器组之间数据的处理方式。您的编译器可能会做聪明的事情,也可能不会做。至于正火载体,我这里写了我的结果:bit.ly/9W5zoU