给出以下代码:
#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的编译器资源管理器中查看:https://godbolt.org/g/jv8fcf
如其他答案/评论中所述,using memcmp(a,b,4) < 0
等效于unsigned
big-endian整数之间的比较。它不能像== 0
小端x86 那样高效地内联。
更重要的是,此行为在gcc7 / 8中的当前版本仅查找memcmp() == 0
或!= 0
。即使在大端的目标在那里,这可能直列一样有效的<
或>
,GCC不会去做。(Godbolt最新的big-endian编译器是PowerPC 64 gcc6.3,而MIPS / MIPS64 gcc5.4 mips
是big-endian MIPS,而mipsel
little-endian MIPS。)如果要在将来的gcc a = __builtin_assume_align(a, 4)
上进行测试,请确保gcc不会。不必担心非x86上的未对齐负载性能/正确性。(或仅使用const int32_t*
代替const char*
。)
如果/当gcc学习内联memcmp
而不是EQ / NE时,也许当gcc的启发式技术告诉它额外的代码大小是值得的时,gcc会在little-endian x86上执行。例如,使用-fprofile-use
(配置文件引导的优化)进行编译时处于热循环中。
如果您希望编译器在这种情况下能胜任工作,则应分配给a uint32_t
并使用endian-conversion函数,例如ntohl
。但是请确保选择一个可以内联的代码;Windowsntohl
显然具有可编译为DLL调用的。有关此问题的其他答案,请参见一些可移植字节序的东西,以及有人对a的不完美尝试portable_endian.h
,以及它的这个分支。我在一个版本上工作了一段时间,但从未完成/测试过或发布过它。
指针广播可能是未定义的行为,具体取决于您如何写入字节以及char*
指向的内容。如果您不确定严格混叠和/或对齐,请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;
}
我检查了Godbolt,并将其编译为高效的代码(基本上与我在下面的asm中编写的代码相同),尤其是在big-endian平台上,即使使用了旧版gcc。它也比ICC17更好,后者内联memcmp
但仅到字节比较循环(即使是这种== 0
情况),代码也更好。
我认为这个手工制作的序列是的最佳实现less4()
(对于x86-64 SystemV调用约定,如在问题中使用的const char *a
in rdi
和b
in 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的单指令。
与== 0
情况相比,必须对两个操作数进行交换会产生额外的代码大小开销:我们无法将其中之一的负载折叠到内存操作数中cmp
。(这节省了代码大小,并且由于微融合而节省了代码。)这是两条额外的bswap
说明之上。
在支持的CPU上movbe
,它可以节省代码大小:movbe ecx, [rsi]
是负载+ bswap。在Haswell上,它是2微码,所以大概它解码为与mov ecx, [rsi]
/ 相同的微码bswap ecx
。在Atom / Silvermont上,它可以在加载端口中正确处理,因此,其uops更少,代码大小也更小。
有关为什么xor / cmp / setcc(clang使用的)比cmp / setcc / movzx(gcc的典型值)更好的setcc
原因,请参阅我的xor- zeroing 答案的一部分。
在通常情况下,这会内联到可以在结果上分支的代码中,将setcc + 0-extend替换为jcc;编译器优化了在寄存器中创建布尔返回值的过程。这是内联的另一个优点:库memcmp
确实必须创建一个由调用者测试的布尔返回值,因为没有x86 ABI /调用约定允许在标志中返回布尔条件。(我也不知道有任何非x86调用约定可以做到这一点)。对于大多数库memcmp
实现,根据长度选择策略(可能还会进行对齐检查)也存在大量开销。这可能是相当便宜的,但是对于4号尺寸,这将超过所有实际工作的成本。
本文收集自互联网,转载请注明来源。
如有侵权,请联系 [email protected] 删除。
我来说两句