分析SIMD代码

JBe*_*Fat 8 c c++ sse simd

更新 - 检查下面

将保持尽可能短.如果需要,很高兴添加更多细节.

我有一些用于规范化矢量的sse代码.我正在使用QueryPerformanceCounter()(包装在一个辅助结构中)来衡量性能.

如果我这样衡量

for( int j = 0; j < NUM_VECTORS; ++j )
{
  Timer t(norm_sse);
  NormaliseSSE( vectors_sse+j);
}
Run Code Online (Sandbox Code Playgroud)

我得到的结果通常比仅使用表示向量的4个双精度进行标准规范化更慢(在相同配置中进行测试).

for( int j = 0; j < NUM_VECTORS; ++j )
{
  Timer t(norm_dbl);
  NormaliseDBL( vectors_dbl+j);
}
Run Code Online (Sandbox Code Playgroud)

但是,像这样计时整个循环

{
  Timer t(norm_sse);
  for( int j = 0; j < NUM_VECTORS; ++j ){
    NormaliseSSE( vectors_sse+j );
  }    
}
Run Code Online (Sandbox Code Playgroud)

显示SSE代码要快一个数量级,但不会真正影响双版本的测量.我做了一些实验和搜索,似乎无法找到合理的答案.

例如,我知道在将结果转换为浮动时可能存在惩罚,但这些都不会发生.

有人可以提供任何见解吗?在每个规范化之间调用QueryPerformanceCounter会如何减慢SIMD代码的速度呢?

谢谢阅读 :)

更多细节如下:

  • 两种规范化方法都是内联的(在反汇编中验证)
  • 在发布中运行
  • 32位编译

简单的矢量结构

_declspec(align(16)) struct FVECTOR{
    typedef float REAL;
  union{
    struct { REAL x, y, z, w; };
    __m128 Vec;
  };
};
Run Code Online (Sandbox Code Playgroud)

规范化SSE的代码:

  __m128 Vec = _v->Vec;
  __m128 sqr = _mm_mul_ps( Vec, Vec ); // Vec * Vec
  __m128 yxwz = _mm_shuffle_ps( sqr, sqr , 0x4e ); 
  __m128 addOne = _mm_add_ps( sqr, yxwz ); 
  __m128 swapPairs = _mm_shuffle_ps( addOne, addOne , 0x11 );
  __m128 addTwo = _mm_add_ps( addOne, swapPairs ); 
  __m128 invSqrOne = _mm_rsqrt_ps( addTwo ); 
  _v->Vec = _mm_mul_ps( invSqrOne, Vec );   
Run Code Online (Sandbox Code Playgroud)

用于规范化双精度的代码

double len_recip = 1./sqrt(v->x*v->x + v->y*v->y + v->z*v->z);
v->x *= len_recip;
v->y *= len_recip;
v->z *= len_recip;
Run Code Online (Sandbox Code Playgroud)

助手结构

struct Timer{
  Timer( LARGE_INTEGER & a_Storage ): Storage( a_Storage ){
      QueryPerformanceCounter( &PStart );
  }

  ~Timer(){
    LARGE_INTEGER PEnd;
    QueryPerformanceCounter( &PEnd );
    Storage.QuadPart += ( PEnd.QuadPart - PStart.QuadPart );
  }

  LARGE_INTEGER& Storage;
  LARGE_INTEGER PStart;
};
Run Code Online (Sandbox Code Playgroud)

更新 所以感谢Johns的评论,我想我已经设法确认QueryPerformanceCounter对我的simd代码做了坏事.

我添加了一个直接使用RDTSC的新计时器结构,它似乎给出了我期望的结果.结果仍然比整个循环的计时慢得多,而不是分别进行每次迭代,但我希望这是因为获取RDTSC涉及刷新指令管道(有关更多信息,请查看http://www.strchr.com/performance_measurements_with_rdtsc).

struct PreciseTimer{

    PreciseTimer( LARGE_INTEGER& a_Storage ) : Storage(a_Storage){
        StartVal.QuadPart = GetRDTSC();
    }

    ~PreciseTimer(){
        Storage.QuadPart += ( GetRDTSC() - StartVal.QuadPart );
    }

    unsigned __int64 inline GetRDTSC() {
        unsigned int lo, hi;
        __asm {
             ; Flush the pipeline
             xor eax, eax
             CPUID
             ; Get RDTSC counter in edx:eax
             RDTSC
             mov DWORD PTR [hi], edx
             mov DWORD PTR [lo], eax
        }

        return (unsigned __int64)(hi << 32 | lo);

    }

    LARGE_INTEGER StartVal;
    LARGE_INTEGER& Storage;
};
Run Code Online (Sandbox Code Playgroud)

Joh*_*nck 13

当只有运行循环的SSE代码时,处理器应该能够保持其流水线满,并且每单位时间执行大量的SIMD指令.当你在循环中添加定时器代码时,现在每个易于优化的操作之间都有一大堆非SIMD指令,可能不太可预测.QueryPerformanceCounter调用可能要么昂贵,要么使数据操作部分无关紧要,要么它执行的代码的性质会严重破坏处理器以最大速率执行指令的能力(可能是由于缓存驱逐或分支是没有很好的预测).

您可以尝试在Timer类中注释掉对QPC的实际调用,并查看它是如何执行的 - 这可以帮助您发现是否构造和销毁问题的Timer对象,或QPC调用.同样,尝试直接在循环中调用QPC,而不是制作一个Timer,看看它是如何比较的.

  • 由于各种原因,QPC通常不用RDTSC实现.因此,QPC的开销很高,并且"QPC不做浮点"的说法令人怀疑. (2认同)