为什么有时仅将memcmp(a,b,4)优化为uint32比较?


69

给出以下代码:

x86_64上的GCC 7对第一种情况进行了优化(Clang做了很长时间了):

但是第二种情况仍然是memcmp()

是否可以将类似的优化应用于第二种情况?最好的组装方法是什么?是否有明确的原因为什么不完成(通过GCC或Clang)呢?

在Godbolt的编译器资源管理器中查看它:https ://godbolt.org/g/jv8fcf


1
我发现有趣的是随随便便无视对齐;这可能适用于x86,但在其他CPU上的优化可能无效。
Matthieu M.

19
@MatthieuM。它仅在目标体系结构上有效
Caleth

@Caleth:同意,但这让我想知道转换是在哪个阶段完成的。也就是说,gcc是否在其中端使用目标特定的优化(也许是抽象的),或者是降低性能的一部分。
Matthieu M.

9
@MatthieuM。您可以通过-fdump-tree-all -fdump-rtl-all(除所有其他开关之外)进行编译来查找。在每个优化阶段之后,这会将中间表示转储到当前工作目录中的文件中,该文件已编号,以便您可以按顺序阅读它们。(如果这样做,您将获得大约300个文件。“ tree”转储比“ RTL”转储更容易阅读。您可能希望略读内部手册的“ RTL”和“机器描述”一章。尝试读取RTL转储之前。)
zwol

Answers:


14

如其他答案/评论中所述,usingmemcmp(a,b,4) < 0等效于unsignedbig-endian整数之间的比较。它无法像== 0小端字节x86那样高效地内联。

更重要的是,此行为在gcc7 / 8中的当前版本仅查找memcmp() == 0!= 0。即使在大端的目标在那里,这可能直列一样有效的<>,GCC不会去做。(Godbolt最新的big-endian编译器是PowerPC 64 gcc6.3,而MIPS / MIPS64 gcc5.4mips是big-endian MIPS,而mipsellittle-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*指向的内容。如果你不能确定严格的混淆和/或排列,memcpyabytes。大多数编译器擅长优化小型固定尺寸memcpy

我检查了Godbolt,然后将其编译为有效的代码(基本上与我在下面的asm中编写的代码相同),尤其是在big-endian平台上,即使使用了旧的gcc。它也比ICC17更好,后者内联memcmp但仅到字节比较循环(即使是这种== 0情况),代码也更好。


我认为这个手工制作的序列是的最佳实现less4()(对于x86-64 SystemV调用约定,如在问题中使用的const char *ainrdibin rsi)。

从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号尺寸,这将超过所有实际工作的成本。


4
我对GCC源代码进行了一些研究。这种优化是通过handle_builtin_memcmp一个名称不正确的名称实现的tree-ssa-strlen.c,如果我没看错的话,它只会实现==and!=情况:如果比较结果不正确,则对2102-3和2108-9行的检查将使其退出,而不进行任何操作。 tEQ_EXPRNE_EXPR,表示它们听起来像什么。后来它也解决了,!SLOW_UNALIGNED_ACCESS (mode, align)这是否意味着“我们可以在不担心对齐的情况下进行此加载吗?”
zwol

1
@zwol:谢谢!我并不惊讶于仅处理==/!=比较此新功能的第一个实现。遗憾的是,没有真正可移植的字节序函数可以使以易于less4编译的方式轻松编写#ifdefs而不会造成混乱。
彼得·科德斯

2
顺便说一句,别名规则是不对称的:char *可以别名任何东西,但int *正式地不能别名char,至少当char声明为as时是这样;参见stackoverflow.com/questions/30967447/…–
zwol

1
FWIW便携式代码片段库具有一个endian模块,该模块似乎可以“快速”完成该工作(该库的其余部分也是如此),并且看起来质量很高并且可以主动维护。
BeeOnRope

73

如果您为little-endian平台生成代码,则将memcmp不等式优化为四个字节以进行单个DWORD比较是无效的。

memcmp它进入单个字节比较低寻址的字节到高字节寻址与平台无关的。

为了memcmp返回零,所有四个字节必须相同。因此,比较的顺序无关紧要。因此,DWORD优化是有效的,因为您忽略了结果的符号。

但是,当 memcmp返回正数时,字节顺序很重要。因此,使用32位DWORD比较实现相同的比较需要特定的字节序:平台必须为big-endian,否则比较结果将不正确。


12
bswapx86中有一条指令,ARM有rev。不过,还有一条额外的说明。
MSalters

4
@CodyGray:作为dasblinkenlight指出,这足以告诉<0>0分开。算术地,CMP寻找最高有效位差,而memcmp寻找存储器顺序中的第一个不同字节。在大端系统上,第一个字节保存MSB。bswap将本地的小尾数位模式转换为大尾数,这是为什么。
MSalters

13
@Kevin:无论如何,您都不想交换内存中的字节(然后恢复它们)!最佳的asm可能是这样的:将两个4B块都装入寄存器,然后对它们进行字节交换。因此,对于解码器来说,这是额外的指令;对于前端来说,这是额外的融合域uops,因为cmp对于这种== 0情况,您不能使用带有like的内存操作数。如 mov edi, [rdi]/ mov esi, [rsi]/ bswap edi/ bswap esi/ cmp edi, esi/濑和MOVZX。这些都是最近的所有Intel和AMD CPU(agner.org/optimize)上的单指令说明
Peter Cordes

5
@Kevin和const-correct仅适用于源代码。只要最终结果相同,CPU便可以做任何喜欢的事情。
OrangeDog

2
@OrangeDog-并非如此。如果声明了参数,const char *那么也很可能已经定义了const它们,这意味着它们可能是只读的,尝试修改它们会导致错误。在现实世界中,这就是声明的事情所发生的一切const char *:将它们放置在.rodata没有写权限的情况下装入的段中。在asm级别上工作无济于事。
BeeOnRope

24

字节顺序是这里的问题。考虑以下输入:

如果通过将它们视为32位整数来比较这两个数组,则会发现它a更大(因为0x03000001> 0x02000002)。在大端机上,此测试可能会按预期进行。


3
没错,但问题在于如何优化memcmp()通话。仍然可以通过在比较之前发出字节交换指令来完成,对吗?
John Zwinck

3
@JohnZwinck我认为为此进行字节交换将处理一个非常特殊的情况,编译器编写者不必理会。
Ruslan

13
@Ruslan:编译器充满了如此小的优化;我很确定编译器开发人员很乐意收到一个补丁来解决这个问题……如果它确实有效的话。
Matthieu M.

1
@MatthieuM .:如果这在实际代码中很重要,则可以通过使用endian.h或类似的字节交换函数uint32_t来比较两个整数,从当前的编译器中获得更好的结果。 请参阅我的答案作为示例。但是,如果编写可移植的代码,则必须担心未对齐的负载和填充,因此,如果可以使用比较未对齐的大端整数memcmp并获得最佳代码,那就太好了。
彼得·科德斯

-2

字节序是一个问题,但带符号的char是另一个问题。例如,考虑比较的四个字节为0x207f2020和0x20802020。签名字符为80时为-128,签名字符为7f时为+127。但是,如果您比较这四个字节,则没有比较会给您正确的顺序。

当然,您可以使用0x80808080进行异或运算,然后可以使用无符号比较。


10
memcmpunsigned char不论是否char已签名,都必须与进行比较。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.