鉴于此代码:
#include <string.h>
int equal4(const char* a, const char* b)
{
return memcmp(a, b, 4) == 0;
}
int less4(const char* a, const char* b)
{
return memcmp(a, b, 4) < 0;
}
x86_64上的GCC 7引入了第一种情况的优化(Clang已经做了很长时间):
mov eax, DWORD PTR [rsi]
cmp DWORD PTR [rdi], eax
sete al
movzx eax, al
但第二种情况仍然是memcmp()
:
sub rsp, 8
mov edx, 4
call memcmp
add rsp, 8
shr eax, 31
是否可以对第二种情况应用类似的优化?什么是最好的装配,有没有明确的理由为什么它没有完成(由GCC或Clang)?
在Godbolt的Compiler Explorer上看到它:https://godbolt.org/g/jv8fcf
答案 0 :(得分:73)
如果为little-endian平台生成代码,则将不等式的四字节memcmp
优化为单个DWORD比较无效。
当memcmp
比较单个字节时,无论平台如何,它都会从低寻址字节变为高寻址字节。
为了使memcmp
返回零,所有四个字节必须相同。因此,比较的顺序无关紧要。因此,DWORD优化是有效的,因为您忽略了结果的符号。
但是,当memcmp
返回正数时,字节排序很重要。因此,使用32位DWORD比较实现相同的比较需要特定的字节序:平台必须是big-endian,否则比较的结果将是不正确的。
答案 1 :(得分:24)
Endianness是这里的问题。考虑一下这个输入:
a = 01 00 00 03
b = 02 00 00 02
如果通过将这两个数组视为32位整数来比较这两个数组,那么您会发现a
更大(因为0x03000001> 0x02000002)。在大端机器上,这个测试可能会按预期工作。
答案 2 :(得分:13)
正如其他答案/评论中所讨论的,使用memcmp(a,b,4) < 0
相当于大端整数之间的unsigned
比较。它无法像小端x86上的== 0
一样有效内联。
更重要的是,gcc7 / 8 only looks for memcmp() == 0
or != 0
中此行为的当前版本。即使在一个大端目标上,这可以为<
或>
提供同样有效的内容,但gcc也不会这样做。 (Godbolt的最新大端编译器是PowerPC 64 gcc6.3,MIPS / MIPS64 gcc5.4。mips
是big-endian MIPS,而mipsel
是小端MIPS。)如果测试这个未来的gcc,使用a = __builtin_assume_align(a, 4)
来确保gcc不必担心非x86上的未对齐加载性能/正确性。 (或者只使用const int32_t*
代替const char*
。)
如果/当gcc学习内联memcmp
以寻找除EQ / NE之外的其他情况,那么gcc可能会在小端x86上进行,当它的启发式告诉它额外的代码大小是值得的。例如在使用-fprofile-use
(配置文件引导优化)进行编译时的热循环中。
如果您希望编译器在这种情况下做得很好,您应该分配给uint32_t
并使用像ntohl
这样的字节序转换函数。但请确保选择一个可以内联的内容;显然是Windows has an ntohl
that compiles to a DLL call。对于某些便携式端口内容,请参阅该问题的其他答案,还有someone's imperfect attempt at a portable_endian.h
和此fork of it。我正在研究一个版本一段时间,但从未完成/测试或发布它。
指针转换可能是未定义的行为,depending on how you wrote the bytes and what the char*
points to。如果您不确定严格别名和/或对齐,memcpy
进入abytes
。大多数编译器都擅长优化小的固定大小memcpy
。
// I know the question just wonders why gcc does what it does,
// not asking for how to write it differently.
// Beware of alignment performance or even fault issues outside of x86.
#include <endian.h>
#include <stdint.h>
int equal4_optim(const char* a, const char* b) {
uint32_t abytes = *(const uint32_t*)a;
uint32_t bbytes = *(const uint32_t*)b;
return abytes == bbytes;
}
int less4_optim(const char* a, const char* b) {
uint32_t a_native = be32toh(*(const uint32_t*)a);
uint32_t b_native = be32toh(*(const uint32_t*)b);
return a_native < b_native;
}
I checked on Godbolt,编译为高效代码(基本上与我在下面的asm中写的相同),特别是在big-endian平台上,即使是旧的gcc。它也比ICC17提供了更好的代码,ICC17内联memcmp
但仅限于字节比较循环(即使对于== 0
情况。
我认为这个手工制作的序列是less4()
的最佳实现(对于x86-64 SystemV调用约定,就像问题中使用的const char *a
一样rdi
中的b
和rsi
。
less4:
mov edi, [rdi]
mov esi, [rsi]
bswap edi
bswap esi
# data loaded and byte-swapped to native unsigned integers
xor eax,eax # solves the same problem as gcc's movzx, see below
cmp edi, esi
setb al # eax=1 if *a was Below(unsigned) *b, else 0
ret
自K8和Core2(http://agner.org/optimize/)以来,这些都是关于Intel和AMD CPU的单指令。
必须bswap两个操作数与== 0
情况相比具有额外的代码大小成本:我们无法将其中一个加载折叠到cmp
的内存操作数中。 (这可以节省代码大小,并且可以通过微融合来实现。)这是最多两条bswap
指令。
在支持movbe
的CPU上,它可以保存代码大小:movbe ecx, [rsi]
是一个load + bswap。在Haswell上,它是2 uops,所以可能它解码为与mov ecx, [rsi]
/ bswap ecx
相同的uops。在Atom / Silvermont上,它在加载端口处理,因此它的uops更少,代码更小。
有关为什么xor / cmp / setcc(哪个clang使用)优于cmp / setcc / movzx(gcc的典型值),请参阅the setcc
part of my xor-zeroing answer。
在通常的情况下,这会内联到分支结果的代码中,setcc +零扩展将替换为jcc;编译器优化在寄存器中创建布尔返回值。 这是内联的另一个优点:库memcmp
必须创建一个调用程序测试的整数布尔返回值,因为没有x86 ABI /调用约定允许在标志中返回布尔条件。 (我不知道任何非x86调用约定也可以这样做)。对于大多数库memcmp
实现,根据长度和对齐检查选择策略也会产生很大的开销。这可能相当便宜,但对于4号,这将超过所有实际工作的成本。
答案 3 :(得分:-2)
Endianness是一个问题,但是signed char是另一个。例如,考虑您比较的四个字节是0x207f2020和0x20802020。 80作为签名字符是-128,7f作为签名字符是+127。但是如果你比较四个字节,没有比较会给你正确的顺序。
当然你可以使用0x80808080进行xor,然后你就可以使用无符号比较。