正如其他答案/评论中所讨论的那样,使用 memcmp(a,b,4) < 0
相当于在大端整数之间进行无符号比较。在小端 x86 上,它不能像 == 0
那样高效地内联。
更重要的是,gcc7/8版本中这种行为
仅查找memcmp() == 0
或!= 0
。即使在大端目标平台上,这种行为对于
<
或
>
也可以像内联一样有效,但gcc也不会这样做。(Godbolt的最新大端编译器是PowerPC 64 gcc6.3和MIPS/MIPS64 gcc5.4。
mips
是大端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有一个将编译为DLL调用的ntohl
。请参阅该问题的其他答案以获取一些可移植字节序内容,并查看
某人不完美的portable_endian.h
尝试,以及此
其分支。我曾经研究过一个版本,但从未完成/测试或发布它。
将指针强制转换为const uint32_t*
将导致未定义的行为,如果这些字节不是对齐的uint32_t
或通过char*
写入。 如果您对严格别名和/或对齐方式不确定,请使用memcpy
到abytes
或使用GNU C属性:有关解决方法,请参见另一个关于对齐和严格别名的Q&A。 大多数编译器都很擅长优化小型固定大小的memcpy
。
#include <endian.h>
#include <stdint.h>
static inline
uint32_t load32_native_endian(const void *vp){
typedef uint32_t unaligned_aliasing_u32 __attribute__((aligned(1),may_alias));
const unaligned_aliasing_u32 *up = vp;
return *up;
}
int equal4_optim(const char* a, const char* b) {
uint32_t abytes = load32_native_endian(a);
uint32_t bbytes = load32_native_endian(b);
return abytes == bbytes;
}
int less4_optim(const char* a, const char* b) {
uint32_t a_native = be32toh(load32_native_endian(a));
uint32_t b_native = be32toh(load32_native_endian(b));
return a_native < b_native;
}
我在Godbolt上检查了一下,它编译出高效的代码(基本上与我下面写的汇编代码相同),尤其是对于大端平台和旧版本的gcc。它还比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以来英特尔和AMD CPU上的单操作指令 (http://agner.org/optimize/)。
与== 0
情况相比,必须对两个操作数执行bswap会增加额外的代码大小成本:我们无法将其中一个加载折叠到cmp
的内存操作数中。(这可以节省代码大小和uops,因为微聚合。) 这还要加上两个额外的bswap
指令。
在支持movbe
的CPU上,它可以节省代码大小:movbe ecx,[rsi]
是一种加载+ bswap。 在Haswell上,它是2个uops,因此可能解码为与mov ecx,[rsi]
/ bswap ecx
相同的uops。 在Atom / Silvermont上,它直接处理在加载端口中,因此不仅uops更少而且代码大小更小。
查看我的异或清零答案中的setcc
部分,了解为什么 xor/cmp/setcc(clang 使用)比 cmp/setcc/movzx(gcc 典型用法)更好。
在通常情况下,当这个代码嵌入到根据结果进行分支的代码中时,
setcc+零扩展会被替换为
jcc;编译器会优化掉在寄存器中创建布尔返回值的步骤,这是内联的另一个优点:
库函数memcmp
不需要创建整数布尔返回值,因为没有x86 ABI/调用约定允许在标志位中返回布尔条件(我也不知道有哪些非x86调用约定可以实现)。对于大多数库函数
memcmp
实现来说,选择策略的开销也很大,可能还要检查对齐方式。这可能很便宜,但对于大小为4的情况来说,其成本将超过所有实际工作的总和。
-fdump-tree-all -fdump-rtl-all
编译参数来获得更多信息(除了其他的开关)。这将在每个优化阶段后将中间表示内容转储到当前工作目录中的文件中,编号以便您可以按顺序阅读它们。(如果这样做,您将获得大约 300 个文件。"树" 转储比 "RTL" 转储更容易阅读。在尝试阅读 RTL 转储之前,您可能需要简略地浏览一下"内部手册"中的 "RTL" 和 "机器描述" 章节。) - zwol