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

约翰·温克(John Zwinck):

给出以下代码:

#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

彼得·科德斯(Peter Cordes):

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

更重要的是,此行为在gcc7 / 8中的当前版本仅查找memcmp() == 0!= 0即使在大端的目标在那里,这可能直列一样有效的<>,GCC不会去做。(Godbolt最新的big-endian编译器是PowerPC 64 gcc6.3,而MIPS / MIPS64 gcc5.4 mips是big-endian MIPS,而mipsellittle-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 *ain rdibin 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] 删除。

编辑于
0

我来说两句

0 条评论
登录 后参与评论

相关文章

为什么将CFBit定义为UInt32?

如何防止Atmel Studio gcc 6.3.1将4字节的memcmp()优化为4字节的直接比较?

为什么即使实际值在uint32范围内,也无法将interface {}转换为uint32

将uint32写入uint64不是原子的。为什么?

为什么recyclerView的findviewbyposition()有时仅返回null

有时我会看到Tensorflow session.run仅获取优化器,有时会获取优化器和成本。有什么不同?

为什么golang中的符文是int32而不是uint32的别名?

为什么:不同符号的整数的比较只是有时发生?

为什么ARC有时仅保留__block out指针?

为什么有时不能仅引用非静态方法?

为什么我的SectionList有时仅呈现一个部分?

将二进制读取为 uint8、创建 uint32 的滑动窗口并找到 uint32 值 == x 的索引的更快方法是什么?

为什么有时在Linux内核中枚举的第一个成员初始化为0

为什么不使用uint32而不是int64?

为什么有时有时将复制构造函数声明为显式非内联的?

将uint8转换为uint32

uint32,int16,uint8 ..为什么这些常用数据类型未纳入标准

C99编译器为什么不将布尔值的“!a && b”优化为“ a <b”?

为什么这个随机生成器有时返回3位而不是4位?

将[32] byte转换为[8] uint32

为什么我们有时将行为与Java中的类分开

为什么有时将reactjs中的箭头功能视为组件?

为什么有时将布尔值称为“标志”?

为什么有时将2D图像建模为指针(T **)?

在Java中,为什么在创建新对象时有时将父类放在左侧?

为什么有时将OpenGL程序称为在软件中实现?

为什么仅当A和B具有子类关系时才能编译(false?A():B())。test()?

如何将4个UINT8变量连接到一个UINT32变量中?

为什么有时不能使用“ ==”关系运算符代替.equals()方法来比较对象?