我一直在分析我们在英特尔酷睿双核上的一些核心数学,在研究平方根的各种方法时,我注意到了一些奇怪的事情:使用 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分之 1 (这对我的目的来说太多了)。
当我尝试使用 SSE 运算求平方根的倒数,然后使用乘法得到平方根时( x * 1/√x = √x ),这很糟糕。尽管这需要两个相关的操作,但它是迄今为止最快的解决方案,在 1.24ns/float 和精确到 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
——但这里的数组数据结构仅用于测试目的:我真正想要测量的是代码中使用的标量性能不能向量化。)