为什么strcmp不是SIMD优化的?

我试图在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 -oa 

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

  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 ,还是由于某种原因,这是一个无意义的想法?

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

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

但是,Intel在SSE4.2指令集中添加了string处理指令。 这些处理终止的零字节问题。 对他们的一个很好的写作阅读这篇博客文章:

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

在这种情况下,GCC正在使用内build的strcmp 。 如果你想使用glibc的版本,使用-fno-builtin 。 但是你不应该假设GCC内置的strcmp或者glibc的strcmp实现是高效的。 我从经验中得知, GCC内置的memcpy和glibc的memcpy并不像它们那样高效 。

我build议你看看Agner Fog的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 

对于他写的通用版本

这是一个非常简单的解决scheme。 使用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的内置版本。

编辑:关于string长度,Agner的SSE4.2版本读取string之外的15个字节。 他认为,这是很less有问题,因为没有写任何东西。 这对堆栈分配数组不是问题。 对于静态分配的数组,这可能是内存页边界的问题。 为了解决这个问题,他在.data部分后面添加了16个字节到.bss部分。 有关更多详情,请参阅asmlib手册中的1.7string说明和安全注意事项

当deviseC的标准库时,处理大量数据时效率最高的string.h方法的实现对于less量数据是合理有效的,反之亦然。 尽pipe可能存在一些string比较的情况,但是对于SIMD指令的复杂使用可以产生比“天真的实现”更好的性能,在许多现实世界的场景中,被比较的string在前几个字符中是不同的。 在这种情况下,天真的执行可能会产生一个比“更复杂”的方法花在决定如何执行比较上更less的时间。 请注意,即使SIMD代码能够一次处理16个字节,并且在检测到不匹配或string结束条件时停止,它仍然需要执行额外的工作,这相当于在扫描的最后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(); 

尽pipe这个方法有点大,但是在第一种情况下,内联可以让编译器消除代码来检查返回值是否大于零,并且在第二种情况下,删除检查代码t1大于t2。 如果方法是通过间接跳转来调度的话,这种优化是不可能的。

我怀疑在SIMD版本的库函数中,只有很less的计算是没有意义的。 我想像strcmpmemcpy和类似的function实际上是由内存带宽而不是CPU的速度。

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

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

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

我试图编写一个自动vector化的实现,并用GCC和MSVC ++编译器进行testing。

所以,我学到的是:
海湾合作委员会的自动vector化是好的(真棒?)
2. MSVC的自动vector化器比GCC的(不vector化我的包装function)
所有的编译器都拒绝产生PMOVMSKB指令,真的很伤心

结果:
在线-GCC编译版本利用SSE2自动vector化增加〜40%。 在具有Bulldozer架构的Windows机器上,CPU自动vector化代码比在线编译器更快,结果与strcmp的本地实现相匹配。 但是关于这个想法的最好的事情是,至less在ARM&X86上,可以为任何SIMD指令集编译相同的代码。

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

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

链接:
由Coliru在线编译器编译的源代码
汇编+源代码(编译器资源pipe理器)

@PeterCordes,感谢您的帮助。

实际上AVX 2.0会更快

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

而不是依靠一个大的指令,你可以使用大量的SIMD指令,16个32字节的寄存器,在UTF16中,你可以给你265个字符!

双倍于avx512几年!

AVX指令也有高吞吐量。

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

今天在最新的Haswell处理器上,我们有强大的AVX2指令。 AVX2指令在32个字节上运行,大部分布尔/逻辑指令的执行速度为每个指令0.5个周期。 这意味着我们可以在执行一个PCMPESTRI所需的相同时间内执行大约22个AVX2指令。 为什么不试一试呢?

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

我没有看到“优化”像strcmp这样的function。

在应用任何types的并行处理之前,您需要查找string的长度,这将迫使您至less读取一次内存。 当你处于这个状态时,你也可以使用这些数据来进行比较。

如果你想快速处理string,你将需要专门的工具,如有限状态机( lexx想到parsing器)。

至于C ++ std::string ,由于很多原因,它们效率低下,速度慢,所以在比较中检查长度的增益是可以忽略的。