鉴于:
#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
数据相关的分支击败了 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];
,编译器确定这两个数组都是可读的。 (神箭)。但这没有帮助,所以仍然没有。