【问题标题】:Profiling SIMD Code分析 SIMD 代码
【发布时间】:2011-08-14 17:21:06
【问题描述】:

已更新 - 查看下方

将尽可能简短。如果需要,很高兴添加更多详细信息。

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

如果我这样测量

for( int j = 0; j < NUM_VECTORS; ++j )
{
  Timer t(norm_sse);
  NormaliseSSE( vectors_sse+j);
}

我得到的结果通常比使用 4 个双精度表示一个向量(在相同配置下测试)进行标准归一化要慢。

for( int j = 0; j < NUM_VECTORS; ++j )
{
  Timer t(norm_dbl);
  NormaliseDBL( vectors_dbl+j);
}

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

{
  Timer t(norm_sse);
  for( int j = 0; j < NUM_VECTORS; ++j ){
    NormaliseSSE( vectors_sse+j );
  }    
}

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

例如,我知道将结果转换为浮动时可能会受到惩罚,但这里没有发生任何事情。

谁能提供任何见解?在每次归一化之间调用 QueryPerformanceCounter 会大大降低 SIMD 代码的速度是什么原因?

感谢阅读:)

更多详情如下:

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

简单向量结构

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

标准化 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 );   

规范双打的代码

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;

辅助结构

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;
};

更新 所以感谢 Johns cmets,我想我已经设法确认是 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;
};

【问题讨论】:

    标签: c++ c sse simd


    【解决方案1】:

    当只有 SSE 代码运行循环时,处理器应该能够保持其管道满载并在单位时间内执行大量 SIMD 指令。当您在循环中添加计时器代码时,现在在每个易于优化的操作之间有一大堆非 SIMD 指令,可能不太可预测。 QueryPerformanceCounter 调用的成本可能足够高,以至于数据操作部分变得微不足道,或者它执行的代码的性质严重破坏了处理器以最大速率保持执行指令的能力(可能是由于缓存逐出或分支不好预测)。

    您可以尝试在 Timer 类中注释掉对 QPC 的实际调用,看看它是如何执行的——这可能会帮助您发现问题在于 Timer 对象的构造和销毁,还是 QPC 调用。同样,尝试直接在循环中调用 QPC,而不是创建 Timer,然后看看比较。

    【讨论】:

    • 嗨,约翰,感谢您的回答。我尝试了您的建议,正如预期的那样,它绝对是导致性能大幅下降的 QPC 调用。我仍然不完全清楚为什么它会对性能产生如此巨大的影响。
    • 我运行了另一个测试 - 将 QPC 调用替换为另一个函数调用(绝对不是内联的),它对结果的影响远没有 QPC 存在那么重要。所以很明显,调用 QueryPerformanceCounter 有一些特别之处。
    • 由于各种原因,QPC 通常不使用 RDTSC 实现。因此,QPC的开销相当高,“QPC不做浮点”的说法值得怀疑。
    【解决方案2】:

    QPC 是一个内核函数,调用它会导致上下文切换,这在本质上比任何等效的用户模式函数调用更昂贵和更具破坏性,并且肯定会破坏处理器以正常速度处理的能力。除此之外,请记住 QPC/QPF 是抽象,需要它们自己的处理——这可能涉及到 SSE 本身的使用。

    【讨论】:

    • 您好,感谢您的回答。你们是对的,这绝对是 QPC 正在做的事情。我真的很想了解为什么它似乎比标准 SISD 代码更能影响 SIMD 指令。可能与浮点和 SIMD 寄存器之间的交换有关吗?
    猜你喜欢
    • 2011-05-22
    • 2012-08-06
    • 1970-01-01
    • 1970-01-01
    • 2012-10-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-05-04
    相关资源
    最近更新 更多