已给予:

#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

  • 是什么阻止了编译器进一步优化它?
  • 我是不是做了什么阻碍了优化的事情?

推荐答案

Data-dependent branching defeats auto-vectorization in GCC/Clang(但不是典型的国际刑事法院).Trip-count不能在第一次迭代之前计算(在抽象机器中),所以GCC和Clang甚至不会try 使用pcmpeqb/pmovmskb来处理大尺寸.(这就是efficient way to memcmp on x86-64 for large inputs.)

显然,这是一个很难解决的问题,因为在GCC/朗已经有15到20年没有解决这个问题了(从现在到GCC第一次开始做自动矢量化.)

另请参阅100-writing it as a loop that counts mismatches and always touches all elements can give auto-vectorization.(或OR减法而不是总和减法).但这对像4字节这样的小固定大小没有帮助.对于第一个字节不同的1GiB MemcMP来说,完全消除提前输出是一场灾难.(像glibc的SSE2/AVX2版本这样的好的SIMD MemcMP可能会在向量比较结果上每隔64到128字节进行分支.)


显然,也没有习语识别,至少这种写作方式没有.(有formemcpy;GCC和clang可以将简单的复制循环转换为call memcpycall memset,或者这些函数的内联扩展.因此,如果您正在编写自己的代码,则需要-fno-builtin-memcpy,否则您的函数会变成对自身的调用……为实验 Select 一个不同的名称.)

将其编写为无条件接触所有字节的循环可以提供自动向量化,但在GCC的内部可能不会被识别为memcmp个成语.我认为这对于有小问题的好代码来说是必要的,就像我们需要单个dword cmp这样的情况.


Compilers must avoid introducing segfaults by inventing reads past where the abstract machine stops.如果void *data指向保存'z'的1字节缓冲区,则您的手动循环在抽象机器中具有定义良好的行为.读取所有4个字节将超过缓冲区的末尾.

但是,如果数组的任何部分不可访问,则memcmp为UB,因此编译器can触及所有4个字节,而不判断提前输出或指针对齐.(100是的,不像你的循环.)

(在x86-64 ASM中,超过末尾可能会进入未映射的页面,从而导致段错误,这违反了as-if规则.100-是的,但只能在同一页内.这是strlenstrchr的对齐加载可以解决的问题,但对于使用不同对齐的指针向量化strcmp来说,这是一个更大的障碍.)

我没有比较函数args中的两个未知指针,而是更改了test_data调用方,将指针传递给两个全局数组char foo[4], bar[4];,编译器确信这两个数组都是可读的.(Godbolt).但这并没有帮助,所以还是没有.

C++相关问答推荐

C中空终止符后面的数字?

C编译器是否遵循restrict的正式定义?

如何使fputs功能提示错误输入并要求用户重新输入.程序停止而不是请求新的输入

拥有3x3二维数组并访问数组[1][3]等同于数组[2][0]?

Rust FFI--如何用给出返回引用的迭代器包装C风格的迭代器?

C++中矢量类型定义和数据保护的高效解决方案

致命:ThreadSaniizer:在Linux内核6.6+上运行时意外的内存映射

我怎么才能用GCC编译一个c库,让它包含另一个库呢?

使用错误的命令执行程序

在基本OpenGL纹理四边形中的一个三角形中进行渲染

用C++从外部ELF符号读取值

`预期说明符-限定符-列表在‘(三元运算符中的’token`‘之前

计算SIZE_MAX元素的长数组的大小

赋值两侧的后置增量,字符指针

try 判断长整数是否为素数

程序打印一些随机空行

C 错误:对 int 数组使用 typedef 时出现不兼容的指针类型问题

使用 c 中的 write() 函数将非 ASCII 字符写入标准输出

`void foo(int a[static 0]);` 有效吗?

用于内存布局的size命令(文本、数据、bss)