4

这个问题的启发,我比较了三个不同的函数来检查参数指向的 8 个字节是否为零(请注意,在原始问题中,字符与 比较'0',而不是0):

bool f1(const char *ptr)
{    
  for (int i = 0; i < 8; i++)
    if (ptr[i])
      return false;
  return true;
}

bool f2(const char *ptr)
{  
  bool res = true;
  for (int i = 0; i < 8; i++)
    res &= (ptr[i] == 0);
  return res;
}

bool f3(const char *ptr)
{  
  static const char tmp[8]{};
  return !std::memcmp(ptr, tmp, 8);
}

虽然我希望启用优化后的汇编结果相同,但只有memcmp版本被翻译成 x64 上的单个cmp指令。两者f1f2都被翻译成缠绕或未缠绕的循环。此外,这适用于所有 GCC、Clang 和 Intel 编译器-O3

f1有什么理由f2不能优化成一条比较指令吗?对我来说,这似乎是一个非常简单的优化。

现场演示:https ://godbolt.org/z/j48366

4

3 回答 3

3

是否有任何理由无法将 f1 和 f2 优化为单个比较指令(可能带有额外的未对齐负载)?对我来说,这似乎是一个非常简单的优化。

f1中,当为真时循环停止ptr[i],因此并不总是等同于考虑 8 个元素,因为它与其他两个函数的情况相同,或者如果数组的大小小于 8,则直接比较 8 个字节的字(编译器不知道数组的大小):

f1("\000\001"); // no access out of the array
f2("\000\001"); // access out of the array
f3("\000\001"); // access out of the array

对于f2,我同意可以在 CPU 允许从任何地址对齐读取 8 字节字的条件下替换为 8 字节比较,这是 x64 的情况,但可能会引入异常情况,如不寻常情况中所述在 x86 asm 中不安全

于 2020-08-13T10:06:07.460 回答
2

首先,f1在第一个非零字节处停止读取,因此在某些情况下,如果您将指向靠近页面末尾的较短对象的指针传递给它,并且下一页未映射,则不会出错。 正如@bruno 指出的那样,在没有遇到 UB的情况下,无条件读取 8 个字节可能会出错。f1在 x86 和 x64 上的同一页面内读取缓冲区末尾是否安全?)。编译器不知道您永远不会以这种方式使用它;它必须为任何假设的调用者编写适用于所有可能的非 UB 案例的代码。

您可以通过创建函数 arg const char ptr[static 8](但这是 C99 功能,而不是 C++)来解决这个问题,以保证即使 C 抽象机器不会触摸所有 8 个字节也是安全的。然后编译器可以安全地发明读取。(指向 a 的指针struct {char buf[8]};也可以,但如果实际指向的对象不是那样,则不会是严格别名安全的。)


GCC 和 clang 不能自动矢量化在第一次迭代之前行程计数未知的循环。 这样就排除了所有搜索循环f1,即使它检查了一个已知大小的静态数组或其他东西。(不过,ICC 可以像简单的 strlen 实现一样矢量化一些搜索循环。)

f2可以像f3, 一样对 qword进行优化cmp,而不会克服主要的编译器内部限制,因为它总是进行 8 次迭代。事实上,当前的 clang 夜间构建确实优化f2了,感谢@Tharwen 发现了这一点。

识别循环模式并不是那么简单,并且需要编译时间来寻找。 IDK 这种优化在实践中的价值;这就是编译器开发人员在考虑编写更多代码来寻找此类模式时需要权衡的因素。(代码的维护成本和编译时成本。)

该值取决于现实世界中的代码实际上有多少这样的模式,以及当你找到它时节省了多少。在这种情况下,这是一个非常好的节省,因此 clang 寻找它并不疯狂,特别是如果他们具有将 8 字节循环转换为一般 8 字节整数操作的基础设施。


在实践中,memcmp如果这是你想要的,就使用;显然大多数编译器不会花时间寻找像f2. 现代编译器确实可以可靠地内联它,特别是对于 x86-64,其中已知未对齐的加载在 asm 中是安全且高效的。

或者memcpy,如果您认为您的编译器比 memcmp 更有可能具有内置 memcpy,请使用执行别名安全的未对齐加载并进行比较。

或者在 GNU C++ 中,使用 typedef 来表示未对齐的可能别名加载:

bool f4(const char *ptr) {
   typedef uint64_t aliasing_unaligned_u64 __attribute__((aligned(1), may_alias));
    auto val = *(const aliasing_unaligned_u64*)ptr;
    return val != 0;
}

使用 GCC10 -O3在Godbolt 上编译:

f4(char const*):
        cmp     QWORD PTR [rdi], 0
        setne   al
        ret

强制转换 touint64_t*可能会违反alignof(uint64_t),并且可能违反严格混叠规则,除非 指向的实际对象与char*兼容uint64_t

是的,对齐在 x86-64 上确实很重要,因为 ABI 允许编译器基于它做出假设。在极端movaps情况下,真正的编译器可能会发生错误或其他问题。

于 2020-08-13T10:06:29.993 回答
-1

你需要帮助你的编译器来得到你想要的东西......如果你想在一个 CPU 操作中比较 8 个字节,你需要改变你的 char 指针,使它指向实际上是 8 个字节长的东西,比如一个 uint64_t 指针。

如果您的编译器不支持 uint64_t,您可以unsigned long long*改用:

#include <cstdint>

inline bool EightBytesNull(const char *ptr)
{  
    return *reinterpret_cast<const uint64_t*>(ptr) == 0;
}

请注意,这适用于 x86,但不适用于需要严格整数内存对齐的 ARM。

于 2020-08-13T10:05:20.943 回答