我的同事给我展示了一段代码,我认为这段代码不必要,但是实际上它非常重要。我预期大多数编译器会将下面三种尝试判断相等性的方式视为相等:
#include <cstdint>
#include <cstring>
struct Point {
std::int32_t x, y;
};
[[nodiscard]]
bool naiveEqual(const Point &a, const Point &b) {
return a.x == b.x && a.y == b.y;
}
[[nodiscard]]
bool optimizedEqual(const Point &a, const Point &b) {
// Why can't the compiler produce the same assembly in naiveEqual as it does here?
std::uint64_t ai, bi;
static_assert(sizeof(Point) == sizeof(ai));
std::memcpy(&ai, &a, sizeof(Point));
std::memcpy(&bi, &b, sizeof(Point));
return ai == bi;
}
[[nodiscard]]
bool optimizedEqual2(const Point &a, const Point &b) {
return std::memcmp(&a, &b, sizeof(a)) == 0;
}
[[nodiscard]]
bool naiveEqual1(const Point &a, const Point &b) {
// Let's try avoiding any jumps by using bitwise and:
return (a.x == b.x) & (a.y == b.y);
}
但出乎意料的是,只有使用
memcpy
或memcmp
的部分会被GCC转换为一个64位比较。为什么?(https://godbolt.org/z/aP1ocs)难道对于优化器来说,如果我检查四个连续字节对的相等性,这就等同于在所有8个字节上进行比较?
试图避免将两个部分分别布尔运算的尝试编译效率稍微提高了一些(指令减少了一个且EDX没有错误依赖),但仍然需要两个32位操作。
bool bithackEqual(const Point &a, const Point &b) {
// a^b == 0 only if they're equal
return ((a.x ^ b.x) | (a.y ^ b.y)) == 0;
}
GCC和Clang在通过值传递结构体时都存在相同的未优化问题(因此a在RDI中,b在RSI中,因为这是x86-64 System V调用约定将结构体打包到寄存器中的方式):https://godbolt.org/z/v88a6s。memcpy/memcmp版本都编译为
cmp rdi, rsi
/sete al
,但其他版本则进行单独的32位操作。令人惊讶的是,在参数位于寄存器中的按值情况下,
struct alignas(uint64_t) Point
仍然有助于优化GCC的两个naiveEqual版本,但不适用于bithack XOR/OR。(https://godbolt.org/z/ofGa1f)。这是否给我们提供了有关GCC内部的任何提示?Clang不受对齐的影响。
return std::memcmp(&a, &b, sizeof(a)) == 0;
来代替优化版本,因为它生成的汇编代码相同且更为表达清晰。 - Ayxan Haqverdilivpmovsxdq
/vmovmskpd
,而不是只使用vmovmskps
/cmp al, 0xf
(高零值在pcmpeqd
输入中将相等,因此总是会设置顶部2位)。甚至可以使用vpmovmskb
; 我们只需要低8位。当然,在这里纯标量显然更好,但如果要查找类似a.x == b.x && a.y!=b.y
的内容,您可以使用clang的SIMD策略,只需使用不同的比较值,例如低2位中的0x1
而不是0x3
。 - Peter Cordesreturn std::bit_cast<std::int64_t>(a) == std::bit_cast<std::int64_t>(b);
是memcpy
/memcmp
的类型安全版本,并且生成相同的优化汇编代码。 - bolovx < 10 && x > 1
会优化成一个 sub / cmp / setbe (无符号下限或等于) 的范围检查 https://godbolt.org/z/G8h3eM 。GCC当然愿意考虑做C抽象机器不会做的工作,特别是如果能在不需要更多指令的情况下完成全部工作(包括从分支源代码到无分支汇编的if-conversion)。甚至有一个答案指出,如果你保证Point
的对齐,GCC实际上会执行所需的优化。 - Peter Cordes