为什么strcmp没有SIMD优化?

时间:2014-10-27 10:59:21

标签: c++ sse simd strcmp sse2

我试图在x64计算机上编译这个程序:

#include <cstring>

int main(int argc, char* argv[])
{
  return ::std::strcmp(argv[0],
    "really really really really really really really really really"
    "really really really really really really really really really"
    "really really really really really really really really really"
    "really really really really really really really really really"
    "really really really really really really really really really"
    "really really really really really really really really really"
    "really really really really really really really really really"
    "really really really really really really really really really"
    "really really really really really really really long string"
  );
}

我这样编译:

g++ -std=c++11 -msse2 -O3 -g a.cpp -o a

但最终的反汇编是这样的:

   0x0000000000400480 <+0>:     mov    (%rsi),%rsi
   0x0000000000400483 <+3>:     mov    $0x400628,%edi
   0x0000000000400488 <+8>:     mov    $0x22d,%ecx
   0x000000000040048d <+13>:    repz cmpsb %es:(%rdi),%ds:(%rsi)
   0x000000000040048f <+15>:    seta   %al
   0x0000000000400492 <+18>:    setb   %dl
   0x0000000000400495 <+21>:    sub    %edx,%eax
   0x0000000000400497 <+23>:    movsbl %al,%eax
   0x000000000040049a <+26>:    retq 

为什么没有使用SIMD?我想可以一次比较16个字符。我应该编写自己的SIMD strcmp,还是出于某种原因这是一个荒谬的想法?

8 个答案:

答案 0 :(得分:42)

在SSE2实现中,编译器应该如何确保字符串末尾没有内存访问?它必须首先知道长度,这需要扫描字符串以查找终止零字节。

如果你扫描字符串的长度,你已经完成了strcmp函数的大部分工作。因此使用SSE2没有任何好处。

但是,英特尔在SSE4.2指令集中添加了字符串处理指令。这些处理终止零字节问题。有关他们的好文章,请阅读此博客文章:

http://www.strchr.com/strcmp_and_strlen_using_sse_4.2

答案 1 :(得分:16)

在这种情况下,GCC使用的是内置strcmp。如果您希望它使用glibc中的版本,请使用-fno-builtin。但是你不应该假设GCC的内置版本strcmp或glibc的strcmp实现是有效的。我根据经验知道GCC's builtin memcpy and glibc's memcpy are not as efficient as they could be

我建议你看看Agner Fog's asmlib。他在汇编中优化了几个标准库函数。请参阅文件strcmp64.asm。它有两个版本:没有SSE4.2的CPU的通用版本和带有SSE4.2的CPU版本。这是SSE4.2版本的主循环

compareloop:
        add     rax, 16                ; increment offset
        movdqu  xmm1, [rs1+rax]        ; read 16 bytes of string 1
        pcmpistri xmm1, [rs2+rax], 00011000B ; unsigned bytes, equal each, invert. returns index in ecx
        jnbe    compareloop            ; jump if not carry flag and not zero flag

对于他写的通用版本

  

这是一个非常简单的解决方案。使用SSE2或任何复杂的

获得的收益并不多

以下是通用版本的主循环:

_compareloop:
        mov     al, [ss1]
        cmp     al, [ss2]
        jne     _notequal
        test    al, al
        jz      _equal
        inc     ss1
        inc     ss2
        jmp     _compareloop 

我会比较GCC的内置strcmp,GLIBC的strcmp和asmlib strcmp的效果。您应该查看反汇编以确保获得内置代码。例如,GCC的memcpy不使用大于8192的内置版本。

编辑: 关于字符串长度,Agner的SSE4.2版本读取超出字符串的15个字节。他认为这很少是一个问题,因为没有写任何东西。这对堆栈分配的数组来说不是问题。对于静态分配的数组,它可能是内存页边界的问题。为了解决这个问题,他在.data部分后面的.bss部分增加了16个字节。有关详细信息,请参阅 asmlib 的manaul中的 1.7字符串说明和安全预防措施部分。

答案 2 :(得分:7)

当设计用于C的标准库时,在处理大量数据时最有效的string.h方法的实现对于少量数据将是合理有效的,反之亦然。虽然可能存在一些字符串比较方案,但是复杂使用SIMD指令可以产生比“天真实现”更好的性能,在许多现实场景中,比较的字符串在前几个字符中会有所不同。在这种情况下,天真的实现可以产生比“更复杂”的方法花费更少的时间来决定应该如何执行比较的结果。请注意,即使SIMD代码一次能够处理16个字节并且在检测到不匹配或字符串结束条件时停止,它仍然需要执行额外的工作,相当于在扫描的最后16个字符上使用朴素方法。如果许多16字节的组匹配,则能够快速扫描它们可能有益于性能。但是如果前16个字节不匹配,那么从逐个字符的比较开始会更有效。

顺便提一下,“天真”方法的另一个潜在优势是可以将其内联定义为标题的一部分(或者编译器可能认为自己具有关于它的特殊“知识”)。考虑:

int strcmp(char *p1, char *p2)
{
  int idx=0,t1,t2;
  do
  {
    t1=*p1; t2=*p2;
    if (t1 != t2)
    {
      if (t1 > t2) return 1;
      return -1;
    }
    if (!t1)
      return 0;
    p1++; p2++;
  } while(1);
}
...invoked as:
if (strcmp(p1,p2) > 0) action1();
if (strcmp(p3,p4) != 0) action2();

虽然该方法有点大,但在第一种情况下,内联可以允许编译器消除代码以检查返回值是否大于零,并在第二种情况下消除代码检查t1是否大于t2。如果通过间接跳转调度方法,则无法进行此类优化。

答案 3 :(得分:3)

我怀疑SIMD版本的库函数中没有任何意义,计算量很小。我想像strcmpmemcpy和类似的函数实际上受内存带宽而不是CPU速度的限制。

答案 4 :(得分:3)

这取决于您的实施。在MacOS X上,memcpy,memmove和memset等功能具有根据您使用的硬件进行优化的实现(相同的调用将根据处理器执行不同的代码,在启动时设置);这些实现使用SIMD 大量(兆字节)使用一些相当花哨的技巧来优化缓存使用。据我所知,strcpy和strcmp没什么。

说服C ++标准库使用这种代码很困难。

答案 5 :(得分:2)

制作strcmp的SSE2版本对我来说是一个有趣的挑战 由于代码膨胀,我不太喜欢编译器内部函数,所以我决定选择自动向量化方法。我的方法基于模板,并将SIMD寄存器近似为不同大小的单词数组。

我尝试编写自动矢量化实现并使用GCC和MSVC ++编译器对其进行测试。

所以,我学到的是:
1. GCC的自动矢量化器很好(真棒吗?)
2. MSVC的自动矢量化程序比GCC更差(不会对我的打包功能进行矢量化)
3.所有编译器拒绝生成PMOVMSKB指令,这真的很难过

结果:
在线-GCC编译的版本通过SSE2自动矢量化获得约40%。在具有Bulldozer架构的Windows机器上,CPU自动矢量化代码比在线编译器更快,结果与strcmp的本机实现相匹配。但关于这个想法的最好的事情是,可以为任何SIMD指令集编译相同的代码,至少在ARM&amp; X86。

注意:
如果有人会找到一种方法来使编译器生成PMOVMSKB指令,那么整体性能应该得到显着提升。

GCC的命令行选项:-std = c ++ 11 -O2 -m64 -mfpmath = sse -march = native -ftree-vectorize -msse2 -march = native -Wall -Wextra

链接:
Source code compiled by Coliru online compiler
Assembly + Source code (Compiler Explorer)

@PeterCordes,谢谢你的帮助。

答案 6 :(得分:0)

实际上AVX 2.0会更快

编辑:它与寄存器和IPC相关

您可以使用大量的SIMD指令和16个32字节的寄存器,而不是依赖于1条大指令,而且,在UTF16中,它可以为您提供265个字符!

在几年内使用avx512加倍!

AVX指令也具有高吞吐量。

根据此博客:https://blog.cloudflare.com/improving-picohttpparser-further-with-avx2/

  

今天在最新的Haswell处理器上,我们拥有强大的AVX2   说明。 AVX2指令在32字节和大部分字节上运行   布尔/逻辑指令以0.5个周期的吞吐量执行   每条指令。这意味着我们可以执行大约22个AVX2   指令执行单个时间相同的时间   PCMPESTRI。为什么不试一试?

编辑2.0 SSE / AVX单元是电源门控的,将SSE和/或AVX指令与常规指令混合涉及一个性能损失的上下文切换,你不应该使用strcmp指令。

答案 7 :(得分:-3)

我没有看到&#34;优化&#34;像strcmp这样的函数。

在应用任何类型的并行处理之前,您需要找到字符串的长度,这将迫使您至少读取一次内存。当你了解它时,你也可以使用这些数据来动态地进行比较。

如果你想快速使用字符串,你需要专门的工具,比如有限状态机(解析器会想到lexx)。

对于C ++ std::string,由于种种原因,它们效率低且速度慢,因此在比较中检查长度的收益是可以忽略的。