【问题标题】:linear search through uint64[] with SSE使用 SSE 通过 uint64[] 进行线性搜索
【发布时间】:2014-05-29 09:53:55
【问题描述】:

我正在尝试使用 SSE 通过 uint64 数组实现线性搜索 指示。我得到了适用于 uint16 和 uint32 的东西,但我得到了编译器 uint64 代码错误(linux、gcc - 请参阅末尾的规范)。

我正在尝试比较 2x2 64 位数字,然后以某种方式翻译结果 在我的数组的索引中。这适用于 uint32(学分转到 http://schani.wordpress.com/2010/04/30/linear-vs-binary-search/):

#include <xmmintrin.h>
#include <smmintrin.h>

typedef ham_u64_t vec2uint64 __attribute__ ((vector_size (16)));
typedef ham_u32_t vec4uint32 __attribute__ ((vector_size (16)));
typedef float     vec4float  __attribute__ ((vector_size (16)));
typedef ham_u16_t vec8uint16 __attribute__ ((vector_size (16)));
typedef ham_u8_t  vec16uint8 __attribute__ ((vector_size (16)));

// ...

vec4uint32 v1 = _mm_loadu_si128((const __m128i *)&data[start + i + 0]);
vec4uint32 v2 = _mm_loadu_si128((const __m128i *)&data[start + i + 4]);
vec4uint32 v3 = _mm_loadu_si128((const __m128i *)&data[start + i + 8]);
vec4uint32 v4 = _mm_loadu_si128((const __m128i *)&data[start + i + 12]);

vec4uint32 cmp0 = _mm_cmpeq_epi32(key4, v1);
vec4uint32 cmp1 = _mm_cmpeq_epi32(key4, v2);
vec4uint32 cmp2 = _mm_cmpeq_epi32(key4, v3);
vec4uint32 cmp3 = _mm_cmpeq_epi32(key4, v4);

vec8uint16 pack01 = __builtin_ia32_packssdw128(cmp0, cmp1);
vec8uint16 pack23 = __builtin_ia32_packssdw128(cmp2, cmp3);
vec16uint8 pack0123 = __builtin_ia32_packsswb128(pack01, pack23);

int res = __builtin_ia32_pmovmskb128(pack0123);
if (res > 0) {
  int czt = __builtin_ctz(~res + 1);
  return (start + i + czt);
}

这是我目前为 uint64 想出的内容。比较有效,我只是 不知道如何处理结果,以及 __builtin_ia32_packssdw() 调用 不编译:

vec2uint64 v1 = _mm_loadu_si128((const __m128i *)&data[start + i + 0]);
vec2uint64 v2 = _mm_loadu_si128((const __m128i *)&data[start + i + 2]);

vec2uint64 cmp0 = _mm_cmpeq_epi64(key2, v1);
vec2uint64 cmp1 = _mm_cmpeq_epi64(key2, v2);

vec4uint32 pack01 = __builtin_ia32_packssdw(cmp0, cmp1); // error
vec4uint32 pack23 = _mm_set1_epi32(0);
vec16uint8 pack0123 = __builtin_ia32_packsswb128(pack01, pack23);

int res = __builtin_ia32_pmovmskb128(pack0123);
if (res > 0) {
  int czt = __builtin_ctz(~res + 1);
  return (start + i + czt);
}

错误提示:

error: cannot convert 'vec1uint64 {aka __vector(2) long unsigned int}'
to '__vector(2) int' for argument '1' to '__vector(4) short int
__builtin_ia32_packssdw(__vector(2) int, __vector(2) int)'

(vec2uint64 的类型定义在顶部,在 uint32 的代码中。)

我的环境:

Linux ws4484 3.5.0-48-generic #72~precise1-Ubuntu SMP Tue Mar 11 20:09:08 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux

gcc version 4.6.3 (Ubuntu/Linaro 4.6.3-1ubuntu5)

我的问题不仅仅是我如何修复编译器错误,而是如果有人有更好的想法来获得匹配的数组索引,也许没有整个 打包东西?

提前致谢!

【问题讨论】:

  • 你为什么使用低级内在函数,例如 __builtin_ia32_packssdw 而不是更常见的 _mm_packs_epi32 ?它使处理向量类型变得更加混乱。
  • 无具体原因;我不知道_mm_packs_epi32。感谢您的评论,我会替换它。
  • 如果您始终使用普通的高级内在函数,那么您可以将__m128i 用于所有向量变量,而不必担心混合不同类型的 32 位和 64 位操作,正如您所见,会变得丑陋。
  • 我听从了你的建议。感谢您的帮助和您的 cmets!
  • 你是用C还是C++写的?

标签: c++ c search sse linear-search


【解决方案1】:

这是在 64 位值中搜索匹配项的完整解决方案。在这种情况下,值 (namehash) 是一个结构成员。该例程在每次迭代中比较 8 个 64 位值并提供匹配的结构索引。

//ptr is a struct array
__m128i key2 = _mm_set1_epi64x(k); //k is the 64 bit search key
for(;;)
   {
   if(!ptr[i].namehash)return NULL;
   __m128i v1 = _mm_set_epi64x (ptr[i+1].namehash,ptr[i].namehash);
   __m128i v2 = _mm_set_epi64x (ptr[i+3].namehash,ptr[i+2].namehash);
   __m128i v3 = _mm_set_epi64x (ptr[i+5].namehash,ptr[i+4].namehash);
   __m128i v4 = _mm_set_epi64x (ptr[i+7].namehash,ptr[i+6].namehash);

   __m128i cmp0 = _mm_cmpeq_epi64(key2, v1);
   __m128i cmp1 = _mm_cmpeq_epi64(key2, v2);
   __m128i cmp2 = _mm_cmpeq_epi64(key2, v3);
   __m128i cmp3 = _mm_cmpeq_epi64(key2, v4);

   __m128i L0  = _mm_shuffle_epi32(cmp0,0xD8);
   __m128i H1 = _mm_shuffle_epi32(cmp1,0xD8);
   __m128i L2  = _mm_shuffle_epi32(cmp2,0xD8);
   __m128i H3 = _mm_shuffle_epi32(cmp3,0xD8);

   __m128i pack0 = _mm_unpacklo_epi64(L0,H1);
   __m128i pack1 = _mm_unpacklo_epi64(L2,H3);

   __m128i pack01 = _mm_packs_epi32(pack0,pack1);
   __m128i pack0123 = _mm_packs_epi16(pack01, _mm_setzero_si128());

   res =  _mm_movemask_epi8(pack0123);
   if(res > 0)break;
   i+=8;
   }

int index = i + __builtin_ctz(res);  //The struct table index to the matching struct.

重要提示:struct 数组长度需要是 8 的倍数,至少有 1 个 NULL 尾随成员

或者,如果每次迭代只需要 2 次 64 位比较,上述可以大大简化为:

for(;;)
   {
   if(!ptr[i].namehash)return NULL;
   __m128i v1 = _mm_set_epi64x (ptr[i+1].namehash,ptr[i].namehash);
   __m128i cmp0 = _mm_cmpeq_epi64(key2, v1);
   res =  _mm_movemask_epi8(cmp0);
   if(res > 0)break;
   i+=2;
   }
int ctz = __builtin_ctz(res);
int index = i + (ctz>>5);  //The struct table index to the matching struct.

【讨论】:

  • 对于大型数组,如果您只是将您的 cmp 结果或运算结果一起查看是否有任何匹配项,则可以使内部循环更快。然后在循环之外,使用 shuffle/pack 找出匹配的位置。 (这就像glibc's memchr 所做的那样,但更多的是胜利,因为所有这些洗牌都是昂贵的。)
  • 此外,如果您不想将循环优化为 4x pcmp,则可以通过使用 (__builtin_ctz(res)&gt;&gt;1) 来完成最后一步用零打包+ 3x por + pmovmskb
【解决方案2】:

我建议不要使用内置的内在函数和隐式向量。仅当您不使用非 GCC 内在函数(例如 _mm_cmpeq_epi32)并且只想坚持使用 GCC 时,这才有意义。你可以像这样做你想做的事

__m128i key2 = _mm_set1_epi64x(key);
__m128i v1 = _mm_loadu_si128((const __m128i *)&data[start + i + 0]);
__m128i v2 = _mm_loadu_si128((const __m128i *)&data[start + i + 2]);

__m128i cmp0 = _mm_cmpeq_epi64(key2, v1);
__m128i cmp1 = _mm_cmpeq_epi64(key2, v2);

__m128i low2  = _mm_shuffle_epi32(cmp0,0xD8);  
__m128i high2 = _mm_shuffle_epi32(cmp1,0xD8);      
__m128i pack = _mm_unpacklo_epi64(low2,high2);

__m128i pack01 = _mm_packs_epi32(pack, _mm_setzero_si128());
__m128i pack0123 = _mm_packs_epi16(pack01, _mm_setzero_si128());

int res =  _mm_movemask_epi8(pack0123);

您可能会找到一个更有效的版本来避免打包,但是您必须使用与__builtin_ctz 不同的函数。

我建议使用 32 位整数

__m128i key4 = _mm_set1_epi32(key);
__m128i v1 = _mm_loadu_si128((const __m128i *)&data[start + i + 0]);
__m128i v2 = _mm_loadu_si128((const __m128i *)&data[start + i + 4]);
__m128i v3 = _mm_loadu_si128((const __m128i *)&data[start + i + 8]);
__m128i v4 = _mm_loadu_si128((const __m128i *)&data[start + i + 12]);

__m128i cmp0 = _mm_cmpeq_epi32(key4, v1);
__m128i cmp1 = _mm_cmpeq_epi32(key4, v2);
__m128i cmp2 = _mm_cmpeq_epi32(key4, v3);
__m128i cmp3 = _mm_cmpeq_epi32(key4, v4);

__m128i pack01 = _mm_packs_epi32(cmp0, cmp1);
__m128i pack23 = _mm_packs_epi32(cmp2, cmp3);
__m128i pack0123 = _mm_packs_epi16(pack01, pack23);

int res = _mm_movemask_epi8(pack0123);

【讨论】:

  • 效果很好。还要感谢 32 位建议 - 我已经根据 @Paul R 的建议替换了内在函数。
  • 我猜GCC内置的内在函数还有其他优势。例如。 _mm_cmpeq_epi64 对于 SSE2 需要 SSE4.1,这很复杂,GCC 内部函数会为您解决这个问题。
  • 对于大型数组,如果您只是将您的 cmp 结果或运算结果一起查看是否有任何匹配项,则可以使内部循环更快。然后在循环之外,使用 shuffle/pack 找出匹配的位置。 (这就像glibc's memchr 所做的那样,但更多的是胜利,因为所有这些洗牌都很昂贵。)此外,_mm_packs_epi32 带有零似乎是一种巨大的浪费。如果你想要一个小的位掩码,只需使用_mm_movemask_ps 来获取每个 dword 元素的高位。
【解决方案3】:

我找不到任何将 64 位整数转换为 32 位整数的指令,这是您使用 packssdw 等所需要的。它变得很长而且很乱,但应该可以工作:

所以,我认为解决方案是使用位掩码(位 0、1、2、3:

这些在循环之前:

vec2uint64 mask0 = { 2, 1 };
vec2uint64 mask1 = { 8, 4 };
vec2uint64 zero  = { 0, 0 };

内循环:

vec2uint64 res0 = _mm_and_si128(cmp0, mask0);
vec2uint64 res1 = _mm_and_si128(cmp1, mask1);

vec2uint64 res2 = _mm_or_si128(res0, res1);

然后我们需要将一个新变量的高位洗牌到低位:

vec2uint64 hi0 = _mm_unpackhi_epi64(res0, zero);
vec2uint64 hi1 = _mm_unpackhi_epi64(res1, zero);

vec2uint64 hi2 = _mm_or_si128(hi0, hi1);
vec2uint64 res3 = _mm_or_si128(res2, hi2);

现在 res 的低 64 位 [嗯,低 4 位,其余为零] 是一个位掩码,其中一个值匹配。

int res = _mm_cvtsi128_si32(res3);

(现在您可以像以前一样计算尾随零)。

【讨论】:

  • 将两个 SSE 寄存器中的四个 64 位值打包成一个 SSE 寄存器中的四个 32 位值可以通过三个内在函数来完成:__m128i low2 = _mm_shuffle_epi32(cmp0,0xD8); __m128i high2 = _mm_shuffle_epi32(cmp1,0xD8); _mm_unpacklo_epi64(low2,high2)
  • 我尝试了这个功能,但它并没有为我的所有测试提供一致的结果。它确实找到了结果,但尾随零的数量不再正确。不过感谢您的回答,我很感激。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-04-09
  • 1970-01-01
  • 1970-01-01
  • 2020-12-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多