【问题标题】:Coaxing GCC to emit REPE CMPSB哄骗 GCC 发出 REPE CMPSB
【发布时间】:2018-08-28 17:29:47
【问题描述】:

如何诱使 GCC 编译器以纯 C 语言发出 REPE CMPSB 指令,不使用“asm”和“_emit”关键字,调用包含的库和编译器内部函数?

我尝试了一些类似下面列出的 C 代码,但没有成功:

unsigned int repe_cmpsb(unsigned char *esi, unsigned char *edi, unsigned int ecx) {

    for (; ((*esi == *edi) && (ecx != 0)); esi++, edi++, ecx--); 

    return ecx;
}

在此链接查看 GCC 如何编译它:
https://godbolt.org/g/obJbpq

附言
我意识到无法保证编译器以某种方式编译 C 代码,但无论如何我还是想哄它一下,看看它有多聪明。

【问题讨论】:

  • 大概不使用asm
  • 正确 - 不使用 asm 指令 ;)
  • rep cmpsb 不快;例如,在 Haswell 上,每字节吞吐量 >= 2 个周期。 (agner.org/optimize)。当前 x86 CPU 中只有 rep movsrep stos 支持“快速字符串”。现代 CPU 的“聪明”之处是使用 SSE2 pcmpeqb。 (但是 gcc 和 clang 不知道如何使用在循环进入之前未知的迭代计数来向量化循环;即它们不能向量化搜索循环。但 ICC 可以。)
  • @PeterCordes 有趣的是 instlatx64 says Haswell 可以做 1 bye / cycle,不知道哪个是对的?
  • @harold:这就是说 L1D 中的带宽。所以它们意味着每个字符串的 1 个字节有 2 个周期。 Agner 将其描述为每n = ecx 的周期数,即每次比较的周期数(这就是我所说的每字节的意思;我没有意识到歧义)。 InstLatx64 将rep lodsb 带宽列为 0.5 字节/周期,这与 Agner 的~2n 数字为rep lods 吞吐量一致。

标签: c performance gcc assembly x86


【解决方案1】:

rep cmps 不快;例如,在 Haswell 上,每个计数吞吐量 >= 2 个周期,加上启动开销。 (http://agner.org/optimize)。即使您必须检查匹配项和0 终止符,如果您编写,您也可以获得一个常规的一次字节循环,每个时钟进行 1 次比较(现代 CPU 可以每个时钟运行 2 次加载)仔细看看。

InstLatx64 numbers agree:Haswell 可以为rep cmpsb 管理每个字节 1 个周期,但这是总带宽(即比较每个字符串中的 1 个字节需要 2 个周期)。


在当前的 x86 CPU 中,只有 rep movsrep stos 支持“快速字符串”。 (即在允许对齐和不重叠的情况下,内部使用更广泛的加载/存储的微编码实现。)

现代 CPU 的“聪明”之处在于使用 SSE2 pcmpeqb / pmovmskb。 (但是 gcc 和 clang 不知道如何使用循环进入之前未知的迭代计数来向量化循环;即它们不能向量化搜索循环。但 ICC 可以。)


但是,出于某种原因,gcc 会针对固定短字符串将 repz cmpsb 内联为 strcmp。大概它不知道内联strcmp 的任何更智能的模式,并且启动开销可能仍然比对动态库函数的函数调用的开销要好。也许不是,我没有测试过。无论如何,将某些内容与一堆固定字符串进行比较的代码块中的代码大小并不可怕。

#include <string.h>

int string_equal(const char *s) {
    return 0 == strcmp(s, "test1");
}

gcc7.3 -O3 output from Godbolt

.LC0:
    .string "test1"
string_cmp:
    mov     rsi, rdi
    mov     ecx, 6
    mov     edi, OFFSET FLAT:.LC0
    repz cmpsb
    setne   al
    movzx   eax, al
    ret

如果您不以某种方式对结果进行布尔化,gcc 会使用 seta / setb / sub / movzx 生成 -1 / 0 / +1 结果。 (在 IvyBridge 之前导致 Intel 上的部分寄存器停顿,以及对其他 CPU 的错误依赖,因为它在 setcc 结果上使用 32 位 sub,/facepalm。幸运的是,大多数代码只需要来自strcmp,而不是 3 路)。

gcc 只对固定长度的字符串常量执行此操作,否则它不知道如何设置rcx


memcmp 的结果完全不同:gcc 做得很好,在这种情况下使用 DWORD 和 WORD cmp,没有表示字符串指令。

int cmp_mem(const char *s) {
    return 0 == memcmp(s, "test1", 6);
}

    cmp     DWORD PTR [rdi], 1953719668  # 0x74736574
    je      .L8
.L5:
    mov     eax, 1
    xor     eax, 1          # missed optimization here after the memcmp pattern; should just xor eax,eax
    ret
.L8:
    xor     eax, eax
    cmp     WORD PTR [rdi+4], 49     # check last 2 bytes
    jne     .L5
    xor     eax, 1
    ret

控制这种行为

The manual-mstringop-strategy=libcall 应该强制调用库,但它不起作用。 asm 输出没有变化。

-mno-inline-stringops-dynamically -mno-inline-all-stringops 也没有。

GCC 文档的这一部分似乎已过时。我还没有进一步研究更大的字符串文字,或固定大小但非常量的字符串,或类似的。

【讨论】:

  • 感谢您的长回答。有趣的是,repe cmpsb 可能比循环中的其他代码慢(例如:?),但我认为在代码大小方面没有任何东西可以击败 repe cmpsb。你同意吗?
  • @KarolaN:对于较短的编译时间常量字符串,gcc 的 memcmp 策略 (cmp [mem], imm32) 具有竞争力,如果您计算代码 + 数据大小。但是,立即数的每个字符串字节都有更多的开销。它避免了设置代码,例如mov ecx, 6 是 5 个字节。 (如果您正在优化代码大小,您将使用 xor eax,eax / lea ecx, [rax+6](5 个字节),这样可以避免在 setcc al 之后出现 movzx。当然,通常您只需在标志上进行分支直接而不是使用 setcc,所以如果在不关心性能的情况下优化代码大小,您可能会 push 6 / pop rcx(3 个字节)。
  • 彼得,我想将您的答案标记为答案,但它缺少一个关键部分,即任何纯 C 代码(没有不透明的 libcalls),它将编译为 repe cmpsb。它不必单独重复 cmpsb。 repe cmpsw 或 repe cmpsd 甚至与 GCC 不同的编译器也可以。
  • 我认为顶部的部分解释了为什么 gcc/clang 不希望对它关心的任何当前 CPU 这样做,出于性能原因,这是真正的答案,尤其是对于非小字符串。也许 clang -Oz(代码大小与性能无关)将从中受益,如果有人愿意实现该模式。但我不希望有人已经实现了它,因为找到这样的模式需要很多代码(在编译器中),但它只对代码大小超过速度等极少数情况有用。跨度>
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2022-10-18
  • 1970-01-01
  • 2012-11-19
  • 1970-01-01
  • 2012-05-20
  • 2015-03-24
  • 2020-02-09
相关资源
最近更新 更多