诱使GCC发射REPE CMPSB
如何引导GCC编译器以普通C发出REPE CMPSB指令,而不使用“asm”和“_emit”关键字,调用包含的库和编译器内部函数 我尝试了下面列出的一些C代码,但没有成功:诱使GCC发射REPE CMPSB,c,performance,gcc,assembly,x86,C,Performance,Gcc,Assembly,X86,如何引导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--);
unsigned int repe_cmpsb(unsigned char *esi, unsigned char *edi, unsigned int ecx) {
for (; ((*esi == *edi) && (ecx != 0)); esi++, edi++, ecx--);
return ecx;
}
请参见GCC如何编译此链接:附加说明
我意识到编译器不能保证以某种方式编译C代码,但我还是想哄它玩一玩,看看它有多聪明。
rep cmps
不是很快;例如,Haswell上的每计数吞吐量>=2个周期,加上启动开销。(). 如果仔细编写,即使您必须检查匹配和0
终止符,也可以让一个常规的逐时字节循环以每个时钟1次比较的速度运行(现代CPU可以每个时钟运行2次加载)
:Haswell可以为rep cmpsb
管理每个字节1个周期,但这是总带宽(即2个周期来比较每个字符串中的1个字节)
只有
rep mov
和rep sto
在当前x86 CPU中支持“快速字符串”。(即,当对齐和缺少重叠允许时,内部使用更宽加载/存储的微代码实现。)
现代CPU的“聪明”之处在于使用SSE2pcmpeqb
/pmovmskb
。(但gcc和clang不知道如何使用循环输入之前未知的迭代计数对循环进行矢量化;也就是说,它们无法对搜索循环进行矢量化。不过,ICC可以。)
但是,出于某种原因,gcc将针对短固定字符串
strcmp
内联repz cmpsb
。大概它不知道任何更智能的内联模式strcmp
,并且启动开销可能仍然比调用动态库函数的开销要好。也许不是,我还没有测试过。无论如何,在一个代码块中比较一些东西和一堆固定字符串的代码大小并不可怕
#include <string.h>
int string_equal(const char *s) {
return 0 == strcmp(s, "test1");
}
如果不以某种方式对结果进行布尔化,gcc将使用seta/setb/sub/movzx生成一个-1/0/+1结果。(导致IvyBridge之前的Intel上的部分寄存器暂停,以及对其他CPU的错误依赖,因为它在setcc
结果/facepalm上使用32位sub
。幸运的是,大多数代码只需要strcmp的2路结果,而不是3路)
gcc只对固定长度的字符串常量执行此操作,否则它将不知道如何设置rcx
对于
memcmp
,结果完全不同:gcc做得很好,在这种情况下,使用DWORD和单词cmp
,没有rep字符串指令
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
控制这种行为 表示
-mstringop strategy=libcall
应该强制进行库调用,但它不起作用。asm输出没有变化
-mno内联stringops动态-mno内联所有stringops
GCC文档的这一部分似乎已经过时。我没有进一步研究更大的字符串文本,或固定大小但非常量的字符串,或类似的字符串。不使用
asm
大概?正确-不使用asm指令;)<代码>代表cmpsb速度不快;例如,Haswell上的每字节吞吐量>=2个周期。(). 当前x86 CPU中只有rep-mov
和rep-sto
支持“快速字符串”。现代CPU的“聪明”之处在于使用SSE2pcmpeqb
。(但gcc和clang不知道如何使用循环输入前未知的迭代计数对循环进行矢量化;即,它们不能对搜索循环进行矢量化。ICC可以。)@PeterCordes有趣的是,Insplatx64 Haswell可以做1个字节/周期,我想知道哪一个是正确的?@harold:这表示L1D中的带宽。所以它们意味着每个字符串的1字节有2个周期。Agner将其描述为每个n=ecx
的周期,即每个比较的周期(这就是我所说的每个字节的意思;我没有意识到模糊性)。InstratX64列出了代表lodsb
带宽为0.5字节/周期,这与Agner的~2n
号一致,用于代表lods
吞吐量。感谢您的详细回答。值得注意的是,repe cmpsb可能比循环中的其他一些代码(例如:?)慢,但我认为在代码大小方面,没有任何东西可以超过repe cmpsb。您同意吗?@KarolaN:对于短编译时常量字符串,如果您计算代码+rodata大小,gcc的memcmp
策略(cmp[mem],imm32
)是有竞争力的。但是,使用立即数时,每个字符串字节的开销更大。它避免了设置代码,例如mov ecx,6
为5字节。(如果您正在优化代码大小,您将xor eax,eax
/lea ecx,[rax+6]
(5字节),这避免了setcc al
之后的movzx
。当然,通常您只需直接在标志上分支,而不是使用setcc,因此您可以按6
//code>弹出rcx(3字节)如果在不考虑性能的情况下优化代码大小。Peter,我想将您的答案标记为答案,但它缺少一个关键部分,即任何普通C代码(没有不透明的调用),这将编译为repe cmpsb。它不必仅是repe cmpsb。repe cmpsw或repe cmpsd,甚至是不同于GCC的编译器也可以。我认为顶部的部分解释了为什么GCC/clang出于性能原因不希望对任何当前关注调优的CPU执行此操作,这才是真正的答案,特别是对于n在微小的字符串上。如果有人费心实现该模式,可能clang-Oz
(代码大小与性能无关)会从中受益。但我不希望有人实现了它,因为找到这样的模式需要大量代码(在编译器中),但它只对代码大小过快这样的罕见情况有用。
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