如何解决测试8个后续字节不会转换为单个比较指令
受this question的启发,我比较了三个不同的函数来检查参数所指向的8个字节是否为零(请注意,在原始问题中,字符是与'0'
而不是{{1 }}):
0
尽管我期望启用优化后的汇编结果相同,但是在x64上,只有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
指令。 cmp
和f1
都被转换为缠绕或未缠绕的循环。而且,这适用于所有带有f2
的GCC,Clang和Intel编译器。
为什么没有-O3
和f1
不能被优化为单个比较指令的原因?对我来说,这似乎是一个非常简单的优化。
实时演示:https://godbolt.org/z/j48366
解决方法
是否有任何原因无法将f1和f2优化为单个比较指令(可能带有额外的未对齐负载)?对我来说,这似乎是一个非常简单的优化。
在 f1 中,当ptr[i]
为true时,循环停止,因此与其他两个函数一样直接考虑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的情况,但是可以引入异常情况,如Unusual situations where this wouldn't be safe in x86 asm
中所述 ,首先,f1
在第一个非零字节处停止读取,因此在某些情况下,如果将指针传递给页面末尾附近的较短对象,它不会出错,并且下一页未映射。 @bruno指出,在f1
没有遇到UB的情况下,无条件读取8个字节可能会出错。 (Is it safe to read past the end of a buffer within the same page on x86 and x64?)。编译器不知道您永远不会以这种方式使用它。它必须使代码对于任何假设的调用者都适用于每种可能的非UB情况。
您可以通过使函数arg const char ptr[static 8]
(但这是C99的功能,而不是C ++)来解决此问题,以确保即使C抽象机不能触摸全部8个字节也可以安全。然后,编译器可以安全地创建读取。 (指向struct {char buf[8]};
的指针也可以,但是如果实际指向的对象不是那样,则严格严格地命名是安全的。)
GCC和clang无法自动向量化在第一次迭代之前未知跳闸计数的循环。这样就可以排除所有f1
之类的搜索循环,即使它检查一个已知大小的静态数组。 (不过,ICC可以对某些搜索循环进行矢量化处理,例如朴素的strlen实现。)
您的f2
可以像f3
一样优化为qword cmp
,而不必克服编译器内部的主要限制,因为它总是进行8次迭代。实际上,当前每晚do optimize f2
的c声,谢谢@Tharwen的发现。
识别循环模式并不是那么简单,并且需要花费编译时间来寻找。 IDK这种优化在实践中将具有多大的价值。这是编译器开发人员在考虑编写更多代码以查找此类模式时需要权衡的。 (代码的维护成本和编译时成本。)
该值取决于多少真实世界代码实际上具有这样的模式,以及在找到代码时可以节省多少。在这种情况下,这是一个很好的节省,因此clang寻找它并不疯狂,特别是如果它们具有将8字节的循环通常转换为8字节整数操作的基础结构。
实际上,如果您要使用memcmp
;显然,大多数编译器不会花时间寻找f2
之类的模式。现代编译器确实可靠地内联了它,尤其是对于x86-64来说,未对齐的负载在asm中是安全有效的。
或者,如果您认为编译器比memcmp更可能具有内置的memcpy,则可以使用memcpy
进行别名安全的未对齐加载并进行比较。
或者在GNU C ++中,使用typedef表示未对齐的may-alias负载:
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;
}
f4(char const*):
cmp QWORD PTR [rdi],0
setne al
ret
投射到uint64_t*
可能会违反alignof(uint64_t)
,并且可能违反严格混叠规则,除非char*
指向的实际对象与uint64_t
兼容。 / p>
是的,在x86-64上对齐确实很重要,因为ABI允许编译器基于它进行假设。在极端情况下,真正的编译器可能会发生movaps
错误或其他问题。
-
https://trust-in-soft.com/blog/2020/04/06/gcc-always-assumes-aligned-pointers/
-
Why does unaligned access to mmap'ed memory sometimes segfault on AMD64?
-
Is `reinterpret_cast`ing between hardware SIMD vector pointer and the corresponding type an undefined behavior?是使用
may_alias
的另一个示例(在这种情况下,不使用aligned(1)
,因为隐含长度的字符串可以在任何点结束,因此您需要对确保您的至少包含1个有效字符串字节的块不会跨越页面边界。)此外,Is `reinterpret_cast`ing between hardware SIMD vector pointer and the corresponding type an undefined behavior?
您需要稍微帮助编译器来获得所需的内容...如果要在一个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。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。