为啥 memcmp 比 for 循环检查快得多?

Posted

技术标签:

【中文标题】为啥 memcmp 比 for 循环检查快得多?【英文标题】:Why is memcmp so much faster than a for loop check?为什么 memcmp 比 for 循环检查快得多? 【发布时间】:2014-01-14 05:42:27 【问题描述】:

为什么memcmp(a, b, size) 比:

for(i = 0; i < nelements; i++) 
    if a[i] != b[i] return 0;

return 1;

memcmp 是 CPU 指令还是什么?它一定很深,因为我在循环中使用memcmp 获得了巨大的加速。

【问题讨论】:

使用 -S 编译以查看汇编语言输出并找出答案。在x86 上,正如其他人所提到的,有这方面的说明,但通常这些可以被矢量化。 但是您使用的是什么优化级别?许多编译器可以展开该循环。 【参考方案1】:

memcmp 通常在汇编中实现,以利用许多特定于体系结构的功能,这可以使其比 C 中的简单循环快得多

作为“内置”

GCC 支持memcmp(以及大量其他功能)作为builtins。在 GCC 的某些版本/配置中,对memcmp 的调用将被识别为__builtin_memcmp。 GCC 不会向 memcmp 库函数发出 call,而是发出一些指令来充当函数的优化内联版本。

在 x86 上,这利用了 cmpsb 指令的使用,该指令将一个内存位置的字节字符串与另一个内存位置进行比较。这与repe 前缀相结合,因此将比较字符串,直到它们不再相等,或者计数耗尽。 (正是memcmp 所做的)。

给定以下代码:

int test(const void* s1, const void* s2, int count)

    return memcmp(s1, s2, count) == 0;

Cygwin 上的gcc version 3.4.4 生成以下程序集:

; (prologue)
mov     esi, [ebp+arg_0]    ; Move first pointer to esi
mov     edi, [ebp+arg_4]    ; Move second pointer to edi
mov     ecx, [ebp+arg_8]    ; Move length to ecx

cld                         ; Clear DF, the direction flag, so comparisons happen
                            ; at increasing addresses
cmp     ecx, ecx            ; Special case: If length parameter to memcmp is
                            ; zero, don't compare any bytes.
repe cmpsb                  ; Compare bytes at DS:ESI and ES:EDI, setting flags
                            ; Repeat this while equal ZF is set
setz    al                  ; Set al (return value) to 1 if ZF is still set
                            ; (all bytes were equal).
; (epilogue) 

参考:

cmpsb instruction

作为库函数

memcmp 的高度优化版本存在于许多 C 标准库中。这些通常会利用特定于架构的指令来并行处理大量数据。

在 Glibc 中,有一些版本的 memcmp for x86_64 可以利用以下指令集扩展:

SSE2 - sysdeps/x86_64/memcmp.S SSE4 - sysdeps/x86_64/multiarch/memcmp-sse4.S SSSE3 - sysdeps/x86_64/multiarch/memcmp-ssse3.S

很酷的部分是 glibc 将检测(在运行时)您的 CPU 拥有的最新指令集,并执行为其优化的版本。看到这个来自sysdeps/x86_64/multiarch/memcmp.S的sn-p:

ENTRY(memcmp)
    .type   memcmp, @gnu_indirect_function
    LOAD_RTLD_GLOBAL_RO_RDX
    HAS_CPU_FEATURE (SSSE3)
    jnz 2f
    leaq    __memcmp_sse2(%rip), %rax
    ret 

2:  HAS_CPU_FEATURE (SSE4_1)
    jz  3f  
    leaq    __memcmp_sse4_1(%rip), %rax
    ret 

3:  leaq    __memcmp_ssse3(%rip), %rax
    ret 

END(memcmp)

在 Linux 内核中

Linux 似乎没有针对 x86_64 的memcmp 的优化版本,但它在arch/x86/lib/memcpy_64.S 中为memcpy 提供了优化版本。请注意,它使用 alternatives 基础架构 (arch/x86/kernel/alternative.c) 不仅在运行时决定使用哪个版本,而且实际上 修补自身 仅在启动时做出此决定 -起来。

【讨论】:

rep cmpsb,即。 用非内置版本 (-fno-builtin) 分析内置版本会很有趣。在某些时候,内置版本要慢得多。不知道有没有好转。 rep cmpsb 这样的IIRC指令实际上很慢。 gcc 现在生成对 memcmp 的 libc 版本的调用,它(在 glibc 中)具有优化的 asm 实现(使用 SIMD,而不是 rep cmpsb)。 这并非普遍适用。现代 CPU 具有“快速字符串操作”功能,可将 rep * 版本放回顶部。 Linux 内核会检测您的 CPU 是否支持此功能并实时修补适当的代码。 (虽然这可能只适用于 movsb 和朋友) Marc Glisse 是正确的;但“快速字符串”仅适用于rep,不适用于repz/repnzrep movsb / rep stosb 很快(尤其是在 Ivybridge+ 上使用 ERMSB),但 repz cmpsb 不是。有关指令表,请参见 agner.org/optimize:在 Skylake 上,repz cmps 的运行时间为 &gt;=2n 个周期,占用 &gt;= 8n 个微指令。 (其中n 是元素计数,rcx 如果它到达末尾,即cmpsb 每 2 个周期 1 个字节。)但rep movs 的最佳情况是 1/32B(每个复制 32 个字节)循环)。【参考方案2】:

它通常是一个编译器内在函数,它被翻译成带有用于比较内存块的专门指令的快速汇编。

intrinsic memcmp

【讨论】:

memcmp 是 GCC builtin,而不是内在的。 intrinsic 通常是指对特定 CPU 指令的 C 级访问。 而内在就是它们在 Visual C++ 中的名称【参考方案3】:

memcmp 是 CPU 指令还是什么?

它至少是一个高度优化的编译器提供的内在函数。可能是一条或两条机器指令,具体取决于您未指定的平台。

【讨论】:

以上是关于为啥 memcmp 比 for 循环检查快得多?的主要内容,如果未能解决你的问题,请参考以下文章

为啥字典比列表快得多?

为啥列表理解比附加到列表要快得多?

为啥 KNN 比决策树快得多?

为啥 MySQL JOIN 比 WHERE IN (子查询) 快得多

为啥从大表中查询 COUNT() 比 SUM() 快得多

为啥性能测试显示我的代码列表比数组快得多? [复制]