是什么阻止编译器优化手写的 memcmp()?

问题描述 投票:0回答:1

鉴于:

#include <string.h>
bool test_data(void *data)
{
    return memcmp(data, "abcd", 4) == 0;
}

编译器可以将其优化为:

test_data:
    cmpl    $1684234849, (%rdi)
    sete    %al
    ret

这很好。

但是如果我使用自己的

memcmp()
(不是来自 string.h),编译器无法将其优化为单个
cmpl
指令。相反,它这样做:

static int memcmp(const void *s1, const void *s2, size_t n)
{
    const unsigned char *p1 = s1, *p2 = s2;
    size_t i;

    for (i = 0; i < n; i++) {
        int ret = p1[i] - p2[i];
        if (ret)
            return ret;
    }

    return 0;
}

bool test_data(void *data)
{
    return memcmp(data, "abcd", 4) == 0;
}
test_data:
    cmpb    $97, (%rdi)
    jne     .L5
    cmpb    $98, 1(%rdi)
    jne     .L5
    cmpb    $99, 2(%rdi)
    jne     .L5
    cmpb    $100, 3(%rdi)
    sete    %al
    ret
.L5:
    xorl    %eax, %eax
    ret

链接:https://godbolt.org/z/Kfhchr45a

  • 是什么阻止编译器进一步优化它?
  • 我是否做了一些阻碍优化的事情?
c assembly optimization x86-64 memcmp
1个回答
0
投票

数据相关的分支击败了 GCC/Clang 中的自动矢量化(但不是经典的 ICC)。在第一次迭代之前(在抽象机中)无法计算行程计数,因此 GCC 和 clang 甚至不会尝试使用

pcmpeqb
/
pmovmskb
来处理大尺寸。 (对于大输入来说,这是 memcmp 的有效方法。)

另请参阅如何自动矢量化数组比较函数 - 将其编写为一个循环,计算不匹配并始终接触所有元素可以进行自动矢量化。 (或者用 OR 约简代替求和约简)。但这对于像 4 字节这样的小固定大小没有帮助。

显然也没有习语识别,至少对于这种写法来说是这样。 (有 for

memcpy
;GCC 和 clang 可以将简单的复制循环转换为
memcpy
memset
,或这些函数的内联扩展。)

将其编写为无条件接触所有字节的循环可以提供自动矢量化,但可能不会在 GCC 内部被识别为

memcmp
习语。我认为这对于有小问题的好代码是必要的,就像我们想要一个双字的情况一样
cmp


编译器必须通过在抽象机停止的地方发明读取来避免引入段错误。如果

void *data
指向保存
'z'
的1字节缓冲区,则您的手动循环在抽象机中具有明确定义的行为。读取所有 4 个字节将超出缓冲区末尾。

但是,如果数组的任何部分不可访问,

memcmp
就是 UB,因此编译器可以触摸所有 4 个字节,而不检查提前退出或指针对齐。 (std::memcmp 可以读取第一个差异之后的任何字节吗?是的,与您的循环不同。)

(在 x86-64 asm 中,越过末尾可能会进入未映射的页面,从而导致段错误,违反 as-if 规则。在 x86 上的同一页面内读取越过缓冲区末尾是否安全和 x64? - 是的,但仅限于同一页面内。您可以通过

strlen
strchr
的对齐负载来解决这个问题,但使用不同对齐的指针对
strcmp
进行矢量化时会遇到更大的障碍。)

我没有比较函数参数中的两个未知指针,而是更改了

test_data
调用者以将指针传递给两个全局数组
char foo[4], bar[4];
,编译器确定这两个数组都是可读的。 (神箭)。但这没有帮助,所以仍然没有。

© www.soinside.com 2019 - 2024. All rights reserved.