如其他答案/评论中所述,usingmemcmp(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.4mips
是big-endian MIPS,而mipsel
little-endian MIPS。)如果要在将来的gcca = __builtin_assume_align(a, 4)
上进行测试,请确保gcc不会这样做。 不必担心非x86上的未对齐负载性能/正确性。(或仅使用const int32_t*
代替const char*
。)
如果/当gcc学习内联memcmp
而不是EQ / NE时,也许gcc会在其启发式技术告诉它额外的代码大小是值得的时,在little-endian x86上执行。例如,使用-fprofile-use
(配置文件引导的优化)进行编译时处于热循环中。
如果您希望编译器在这种情况下能胜任工作,则应分配给auint32_t
并使用endian-conversion函数,例如ntohl
。但是请确保选择一个可以内联的代码;Windowsntohl
显然具有可编译为DLL调用的。有关此问题的其他答案,请参见一些可移植字节序的东西,以及有人对a的不完美尝试portable_endian.h
,以及它的这个分支。我在一个版本上工作了一段时间,但从未完成/测试过或发布过它。
指针广播可能是未定义的行为,具体取决于您如何写入字节以及char*
指向的内容。如果你不能确定严格的混淆和/或排列,memcpy
成abytes
。大多数编译器擅长优化小型固定尺寸memcpy
。
#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
inrdi
和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
情况相比,必须bswap交换两个操作数会产生额外的代码大小开销:我们无法将其中之一的负载折叠到内存操作数中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号尺寸,这将超过所有实际工作的成本。