首先,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
情况下,真正的编译器可能会发生错误或其他问题。