2013-07-03 51 views
3

我希望能够帮助优化程序中计算最密集的功能。 目前,我发现基本(非SSE)版本显着更快(最多3倍)。因此,我会请求你帮忙纠正这一情况。优化SSE代码

函数查找无符号整数向量中的子集,并报告它们是否存在。为了您的方便,我只包含了相关的代码片段。

首先是基本变体。它检查block_是否是x.blocks_的一个子集。

//Check for self comparison 
    if (this == &x) 
      return false; 

//A subset is equal to or smaller. 
    if (no_bits_ > x.no_bits_) 
      return false; 

    int i; 

    bool equal = false; 

//Pointers should not change. 
    const unsigned int *tptr = blocks_; 
    const unsigned int *xptr = x.blocks_; 


    for (i = 0; i < no_blocks_; i++, tptr++, xptr++) { 
      if ((*tptr & *xptr) != *tptr) 
        return false; 
      if (*tptr != *xptr) 
        equal = true; 
    } 

    return equal; 

接着是上证所的变体,其中唉根据我的期望不执行。这两个片段都应该寻找相同的东西。

//starting pointers.   
    const __m128i* start = (__m128i*)&blocks_; 
    const __m128i* xstart = (__m128i*)&x.blocks_; 

    __m128i block; 
    __m128i xblock; 

    //Unsigned ints are 32 bits, meaning 4 can fit in a register. 
    for (i = 0; i < no_blocks_; i+=4) { 

      block = _mm_load_si128(start + i); 
      xblock = _mm_load_si128(xstart + i); 

        //Equivalent to (block & xblock) != block 
        if (_mm_movemask_epi8(_mm_cmpeq_epi32(_mm_and_si128(block, xblock), block)) != 0xffff) 
          return false; 

        //Equivalent to block != xblock 
        if (_mm_movemask_epi8(_mm_cmpeq_epi32(block, xblock)) != 0xffff) 
          equal = true; 
    } 
    return equal; 

对于如何改进SSE版本的性能,您有什么建议吗?难道我做错了什么?或者,这是否应该在其他地方进行优化?

我还没有在剩余的计算中加入no_blocks_%4!= 0,但是这样做的目的很小,直到性能提高为止,并且它只会使代码在这一点上混乱起来。

谢谢你提供的任何帮助。

+0

您是否真的了解了编译器为两种情况生成的内容。我发现GCC/G ++首先生成相当不错的SSE代码(并且它更好地避免了我们人类倾向于提出的“寄存器依赖性”)。 –

+0

http://channel9.msdn.com/Events/Build/2013/4-329 –

回答

3

我在这里看到了三种可能性。

首先,您的数据可能不适合广泛的比较。如果(*tptr & *xptr) != *tptr在前几个块内有很高的可能性,那么简单的C++版本几乎肯定会更快。在这种情况下,您的上证所将运行更多的代码&数据来完成相同的事情。

其次,您的SSE代码可能不正确。这里并不完全清楚。如果no_blocks_在两个样本之间相同,则start + i可能具有索引128位元素的不良行为,而不是32位作为第一个样本。

三,SSE 真的喜欢它,当指令可以流水线,这是一个很短的循环,你可能没有得到。您可以通过一次处理多个SSE块来显着减少分支。

这是一次处理2个SSE区块的快速未经测试的镜头。注意我已经完全删除了block != xblock分支,将状态保持在循环之外,并且只在最后进行测试。总的来说,这会将事件从每个int的1.3个分支移动到0.25。

bool equal(unsigned const *a, unsigned const *b, unsigned count) 
{ 
    __m128i eq1 = _mm_setzero_si128(); 
    __m128i eq2 = _mm_setzero_si128(); 

    for (unsigned i = 0; i != count; i += 8) 
    { 
     __m128i xa1 = _mm_load_si128((__m128i const*)(a + i)); 
     __m128i xb1 = _mm_load_si128((__m128i const*)(b + i)); 

     eq1 = _mm_or_si128(eq1, _mm_xor_si128(xa1, xb1)); 
     xa1 = _mm_cmpeq_epi32(xa1, _mm_and_si128(xa1, xb1)); 

     __m128i xa2 = _mm_load_si128((__m128i const*)(a + i + 4)); 
     __m128i xb2 = _mm_load_si128((__m128i const*)(b + i + 4)); 

     eq2 = _mm_or_si128(eq2, _mm_xor_si128(xa2, xb2)); 
     xa2 = _mm_cmpeq_epi32(xa2, _mm_and_si128(xa2, xb2)); 

     if (_mm_movemask_epi8(_mm_packs_epi32(xa1, xa2)) != 0xFFFF) 
      return false; 
    } 

    return _mm_movemask_epi8(_mm_or_si128(eq1, eq2)) != 0; 
} 

如果你有足够的数据和失败的前几个SSE块中的概率很低,这样的事情应该是至少一定程度上比你快SSE。

+0

我已经尝试了两种解决方案。不幸的是,它似乎没有加快速度,而是需要更长的时间。然而,修正索引确实有所帮助。尽管如此,感谢您的时间和精力,因为我已经从这个更聪明的人中脱颖而出,希望更好地提高分支意识。 – Dess

+0

你要喂什么数据?我开始怀疑你真的很早就摆脱了这个循环。 –

+0

是的,事实证明是这样。在检查迭代之后,大多数调用完成循环。但是,我之前没有测试过;那些完成了循环的人,绝大多数都很小(在'no_blocks_ <= 16'处检查)。作为回应,我将尝试执行更大的向量或将向量移动到静态大小,然后使用向量,我希望能够以智能方式进行管道传输。 – Dess

0

我似乎认为你的问题是一个内存带宽有界的问题: 渐近你需要大约2个操作来处​​理扫描的内存中的一对整数。没有足够的算术复杂度来利用CPU SSE指令使用更多的算术吞吐量。事实上,你的CPU通过大量的时间来等待数据传输。 但是,在你的情况下使用SSE指令会导致整个指令,并且编译器不会对生成的代码进行优化。

有一些替代战略,改善带宽约束问题表现为:通过并行运算

  • 多线程隐藏存取存储器超线程情况下 操作。
  • 数据加载大小的精细调整可以提高内存带宽。
  • 通过在循环中添加补充独立操作来提高管道连续性(在“for”循环中的每个步骤扫描两组不同的数据)
  • 将更多数据保存在缓存或寄存器中(某些迭代代码可能需要多次相同的数据集)