如何哄骗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
P.S。
我意识到无法保证编译器以某种方式编译C代码,但我还是想哄它以获得乐趣,只是为了看它有多聪明。
答案 0 :(得分:7)
rep cmps
并不快;例如,Haswell的每个计数吞吐量的> = 2个周期加上启动开销。 (http://agner.org/optimize)。你可以得到一个常规的一次一个字节循环,每个时钟进行1次比较(现代CPU每个时钟可以运行2次加载),即使你必须检查匹配和0
终结器,如果你仔细写下来。
InstLatx64 numbers agree:Haswell可以为rep cmpsb
管理每个字节1个周期,但这是总带宽(即比较每个字符串1个字节的2个周期)。
在当前的x86 CPU中,只有rep movs
和rep 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的错误依赖,因为它在sub
结果/ facepalm上使用32位setcc
。幸运的是,大多数代码只需要2来自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文档的这一部分似乎已经过时了。我没有进一步调查更大的字符串文字,或固定大小但非常量字符串,或类似。