将 16 字节字符串与 SSE 进行比较

Compare 16 byte strings with SSE

我有 16 字节 'strings'(它们可能更短,但您可以假设它们末尾用零填充),但您可能不会假设它们是 16 字节对齐的(至少不总是) .

如何编写将它们与 SSE 内在函数进行比较(为了相等)的例程?我发现这段代码片段可能会有帮助,但我不确定它是否合适?

register __m128i xmm0, xmm1; 
register unsigned int eax; 

xmm0 = _mm_load_epi128((__m128i*)(a)); 
xmm1 = _mm_load_epi128((__m128i*)(b)); 

xmm0 = _mm_cmpeq_epi8(xmm0, xmm1); 

eax = _mm_movemask_epi8(xmm0); 

if(eax==0xffff) //equal 
else   //not equal 

有人可以解释一下或者写一个函数体吗?

它需要在 GCC/mingw 中工作(在 32 位 Windows 上)。

向量比较指令将其结果作为 掩码,根据相应源之间的比较,元素全为 1(真)或全为 0(假)元素。

请参阅 https://whosebug.com/tags/x86/info 以获取一些链接,这些链接将告诉您这些内在函数的作用。

问题中的代码看起来应该可以工作。

如果您想找出 哪些 元素不相等,请使用 movemask 版本(pmovmskbmovmskps)。您可以 tzcnt / bsf 对第一个匹配项进行位扫描,或 popcnt 对匹配项进行计数。完全相等给你一个 0xffff 掩码,不相等给你 0.


您可能想知道 SSE4.1 ptest 在这里是否有用。它是可用的,但实际上并没有更快,特别是如果您对结果进行 分支 而不是将其转换为布尔值 0 / -1。

 // slower alternative using SSE4.1 ptest
__m128i avec, bvec;
avec = _mm_loadu_si128((__m128i*)(a)); 
bvec = _mm_loadu_si128((__m128i*)(b)); 

__m128i diff = _mm_xor_si128(avec, bvec);  // XOR: all zero only if *a==*b

if(_mm_test_all_zeros(diff, diff))  { //equal 
} else {   //not equal 
}

在 asm 中,ptest 是 2 微指令,并且不能与 jcc 条件分支宏融合。因此前端的总 pxor + ptest + 分支是 4 微指令,并且仍然会破坏其中一个输入,除非您有 AVX 将异或结果放入第三个寄存器。

pcmpeqb xmm0, xmm1 / pmovmskb eax, xmm0 / cmp/jcc 总共是 3 微指令,在 Intel 和 AMD CPU 上 cmp/jcc 融合成 1 微指令。

如果您有更宽的元素,您可以在 pcmpeqd/q 的结果上使用 movmskpsmovmskpd 以获得 4 位或 2 位掩码。如果您想在不除以每个元素 4 或 8 个字节的情况下进行位扫描或 popcnt,这将非常有用。 (或者使用 AVX2,8 位或 4 位而不是 32 位掩码。)

ptest 只是一个好主意,如果您不需要任何额外的指令来为其构建输入:测试是否为全零,有或没有掩码。例如检查每个元素或某些元素中的某些位。

好吧,我不确定这是否会更快,但可以通过单个 SSE 4.2 指令完成:检查 PCMPISTRI(压缩比较隐式长度字符串,Return 索引)进位and/or 溢出标志:

if (_mm_cmpistrc(a, b, mode))   // checks the carry flag (not set = equal)
  // equal
else
  // unequal

模式将是(针对您的情况):

const int mode = 
  SIDD_UBYTE_OPS |         // 16-bytes per xmm
  SIDD_CMP_EQUAL_EACH |    // strcmp
  SIDD_NEGATIVE_POLARITY;  // find first different byte

不幸的是,这条指令的文档很少。 因此,如果有人找到合适的资源来汇总所有模式组合和生成的标志,请分享。

我会尽力帮助解决被遗忘的有人可以解释这个问题的一部分吗。

register __m128i xmm0, xmm1; 
register unsigned int eax; 

这里我们声明了一些变量。 __m128i 是 SSE 寄存器整数运算的内置类型。请注意,变量的名称根本无关紧要,但作者将它们命名为与在汇编中调用相应的 CPU 寄存器完全相同。 xmm0xmm1xmm2xmm3、……都是SSE操作的寄存器。 eax 是通用寄存器之一。

register 关键字很久以前就被用来建议编译器将变量放在 CPU 寄存器中。今天它完全没用了,我想。有关详细信息,请参阅 this question

xmm0 = _mm_loadu_si128((__m128i*)(a)); 
xmm1 = _mm_loadu_si128((__m128i*)(b)); 

此代码已按照@harold 的建议进行了修改。这里我们从给定的内存指针(可能未对齐)加载 16 个字节到变量 xmm0xmm1。在汇编代码中,这些变量很可能直接位于寄存器中,因此该内在函数会产生未对齐的内存负载。将指针转换为 __m128i* 类型是必要的,因为 intrinsic 接受这种指针类型,尽管我不知道英特尔为什么这样做。

xmm0 = _mm_cmpeq_epi8(xmm0, xmm1); 

这里我们比较 xmm0 变量中的每个字节与 xmm1 变量中的相应字节是否相等。后缀 _epi8 表示对 8 位元素,即字节进行操作。它有点类似于 memcmp(&xmm0, &xmm1, 16),但会生成其他结果。它 return 是一个 16 字节的值,其中包含 0xFF 代表每个具有相同值的字节,以及 0x00 代表每个具有不同值的字节。

eax = _mm_movemask_epi8(xmm0); 

这是SSE2中的一条非常重要的指令,通常用于编写带有某些SSE条件的if语句。它从 XMM 参数中的 16 个字节中的每一个中取出最高位,并将它们写入一个 16 位整数。在汇编级别,这个数字位于通用寄存器中,允许我们在之后快速检查它的值。

if(eax==0xffff) //equal 
else   //not equal

如果两个 XMM 寄存器的所有 16 个字节都相等,则 _mm_cmpeq_epi8 必须 return 一个设置了所有 128 位的掩码。 _mm_movemask_epi8 将 return 完整的 16 位掩码,即 0xFFFF。如果任何两个比较的字节不同,相应的字节将由 _mm_cmpeq_epi8 填充为零,因此 _mm_movemask_epi8 将 return 16 位掩码与相应的位 not 设置,所以它会小于 0xFFFF.

此外,这里是封装在函数中的解释代码:

bool AreEqual(const char *a, const char *b) {
  __m128i xmm0, xmm1; 
  unsigned int eax; 
  xmm0 = _mm_loadu_si128((__m128i*)(a)); 
  xmm1 = _mm_loadu_si128((__m128i*)(b)); 
  xmm0 = _mm_cmpeq_epi8(xmm0, xmm1); 
  eax = _mm_movemask_epi8(xmm0); 
  return (eax == 0xffff); //equal 
}