使用SIMD指令改写memcpy/memcmp/...有意义吗?

15

在大规模软件中,使用SIMD指令重写memcpy/memcmp等函数是否有意义?

如果有意义,为什么GCC不会默认生成这些库函数的SIMD指令呢?

此外,是否还有其他函数可以通过SIMD进行改进?


这取决于您使用的操作系统和编译器库。例如,Mac OS X已经具有SIMD优化的memcpy et al。此外,英特尔的ICC生成内联memcpy,比您可能能够在库中实现的任何东西都要快。 - Paul R
@Paul:memcpy实际上是SSE内置函数的最坏情况,因为SSE不能用于边缘情况。那些编译器是否会为strlenmemchr发出SIMD代码? - Ben Voigt
@Ben:我刚刚使用ICC 12进行了检查 - memcpy和strlen都会发出内联SSE代码,而strchr是一个库函数,似乎只是直接的标量代码。 - Paul R
5个回答

8
是的,使用SSE指令可以使这些函数更快。如果您的运行时库/编译器内置函数包括优化版本,那就太好了,但这似乎并不普遍。
我有一个自定义的SIMD memchr,比库版本快得多。特别是当我要查找2或3个字符中的第一个字符时(例如,我想知道这行文本中是否有方程式,我搜索第一个出现的=、\n、\r)。
另一方面,库函数经过充分测试,因此只有在您频繁调用它们且分析器显示它们占用了您CPU时间的重要部分时,编写自己的函数才值得。

一个SIMD memcpy通常只有在源和/或目标已经在缓存中的情况下才会更快,因为几乎任何半好的memcpy都应该能够饱和可用的DRAM带宽。 - Paul R
2
@Paul:SIMD总是更好的。即使因为内存访问跟不上而不能严格加速,该核心也可以用于超线程、节能或乱序执行。正如Crashworks所说,SSE还可以通过预取提示更快地将数据提取到缓存中。如果没有SSE,CPU可能必须在提取数据和执行复制之间交替进行,而使用SSE则可以并行进行这两个操作。 - Ben Voigt
在memcpy et al的情况下,执行线程中没有其他事情发生,因此没有任何好处。如果您的核心因等待DRAM访问而停顿,则无法做太多事情 - DRAM延迟可能达到200个时钟周期,这是很多指令周期都没有任务要执行的。 - Paul R
2
@Paul:(1)并非所有的memcpy调用都是针对数千字节的。你可能会在循环中有一个memcpy调用,它只有约20个字节,并且还有其他处理。 (2)现代CPU核心不仅限于处理来自单个线程的指令,这就是我提到超线程的原因。 (3)当读取预取被流水线化时,DRAM延迟就不那么重要了,只有吞吐量才是关键。 (4)即使DRAM吞吐量正在拖累代码,也最好高效地执行复制操作,因为CPU可以在同样的时间内完成工作,并且功耗更低(例如,动态降低时钟频率)。 - Ben Voigt
你在使用什么烂库,竟然没有一个好的SIMD memchr?Glibc有手写汇编版本的memchr / strchr / memmove等函数,适用于i386和x86-64(以及大多数其他ISA),对于大缓冲区非常出色,并且许多还具有良好的小缓冲区策略。 (通过动态链接器符号解析进行运行时分派,因此即使在没有使用-mavx2编译的二进制文件中,它也可以在兼容CPU上使用AVX2)。你能获得的主要优势是,如果你知道你的缓冲区已经对齐并且/或者至少有16个字节长,那么你就可以避免分支来选择策略。 - Peter Cordes
https://code.woboq.org/userspace/glibc/sysdeps/x86_64/multiarch/memchr-avx2.S.html 是 glibc 的 memchr,使用 4 个向量的 vpcmpeqb,然后将它们全部合并为一个向量,以节省 vpmovmskbtest 操作码,每 2 个缓存行循环一次分支。 - Peter Cordes

5

这没有意义。如果你的编译器能够发出SIMD指令,那么它应该隐式地为memcpy / memcmp /类似内部函数发出这些指令。

您可能需要明确指示GCC使用例如-msse -msse2发出SSE操作码;一些GCC默认情况下不启用它们。此外,如果您不告诉GCC进行优化(即-o2),它甚至不会尝试发出快速代码。

像这样使用SIMD操作码对内存工作可以产生巨大的性能影响,因为它们还包括缓存预取和其他DMA提示,这些提示对于优化总线访问非常重要。但这并不意味着您需要手动发出它们;即使大多数编译器通常都很糟糕,但我使用过的每个编译器至少都处理了基本CRT内存函数的操作。

基本数学函数也可以从将编译器设置为SSE模式中受益。您可以轻松获得8倍的加速,只需告诉编译器使用SSE操作码而不是可怕的旧x87 FPU。


同意memcpy最有可能被正确优化。许多其他来自<string.h><memory.h>的函数也极大地受益,但编译器并没有广泛优化它们。 - Ben Voigt
@BenVoigt: GCC并不总是内联库函数的好版本,但好的库有很好的手写汇编。例如为什么启用优化后这段代码慢了6.5倍?展示了一个情况,在-O1级别下GCC内联了一个非常糟糕的repne scasb strlen,或者在-O2级别下内联了一个复杂的32位一次的位操作,它没有利用SSE2的任何优势。该程序完全依赖于strlen处理大缓冲区的性能,因此调用glibc的优化版本对它来说是一个巨大的胜利。库和内联之间存在很大的区别。 - Peter Cordes

0

这可能并不重要。CPU的速度比内存带宽快得多,而编译器运行时库提供的memcpy等实现可能已经足够好了。在“大规模”软件中,性能主要受到内存复制的影响是不太可能的(很可能是受到I/O的影响)。

要获得真正的内存复制性能提升,一些系统具有专门的DMA实现,可用于从内存复制到内存。如果需要显著提高性能,硬件是获得性能提升的途径。


这在很大程度上取决于您是否使用像C++ iostreams这样的可怕缓慢的I/O API。很难以与操作系统提供的I/O速度进行任何非平凡处理。此外,由于各种原因,特别是在较小的块上,SIMD更快,而设置DMA引擎将是代价高昂的。首先,SSE使用不同的CPU寄存器集,因此您的工作变量保持注册状态,不会溢出到缓存中。 - Ben Voigt

0

-1
在x86硬件上,这并不重要,因为有乱序处理。处理器将实现必要的ILP,并尝试每个周期发出最大数量的load/store操作以进行memcpy,无论是SIMD还是标量指令集。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接