C++ 为什么可以';GCC是否为两个Int32的结构生成一个最优运算符==?
一位同事向我展示了我认为不必要的代码,但果然是这样。我预计大多数编译器都会认为这三种平等测试的尝试是等效的:C++ 为什么可以';GCC是否为两个Int32的结构生成一个最优运算符==?,c++,gcc,x86-64,compiler-optimization,micro-optimization,C++,Gcc,X86 64,Compiler Optimization,Micro Optimization,一位同事向我展示了我认为不必要的代码,但果然是这样。我预计大多数编译器都会认为这三种平等测试的尝试是等效的: #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 &&am
#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);
}
GCC和Clang在按值传递结构时都有相同的遗漏优化(因此,
a
在RDI中,b
在RSI中,因为x86-64 System V的调用约定就是这样将结构打包到寄存器的):。memcpy/memcmp版本都编译为cmp-rdi、rsi
/sete-al
,但其他版本执行单独的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;
}
struct alignas(uint64_t)Point
在参数位于寄存器中的按值情况下仍然有帮助,优化了GCC的两个相同版本,但没有优化bithack XOR/OR。(). 这给了我们关于GCC内部的任何提示吗?对齐对Clang没有帮助。如果“修复”对齐,则所有对齐都会提供相同的汇编语言输出(使用GCC):
需要注意的是,一些正确/合法的方法(如类型双关)是使用memcpy
,因此在使用该函数时进行特定优化(或更积极)似乎是合乎逻辑的
为什么编译器不能生成[与memcpy版本相同的程序集]
编译器“可以”的意思是允许它
编译器根本没有。为什么不这样做超出了我的知识范围,因为这需要深入了解Optimizer是如何实现的。但是,在所有目标CPU上,答案可能从“没有包含这种转换的逻辑”到“规则没有调整为假设一个输出比另一个快”
如果使用Clang而不是GCC,您会注意到它为naiveEqual
和naiveEqual1
生成相同的输出,并且该程序集没有跳转。除了使用两条32位指令代替一条64位指令外,它与“优化”版本相同。此外,如Jarod42所示,限制点的对齐对优化程序没有影响
MSVC的行为类似于叮当声,因为它不受对齐的影响,但不同的是,它没有消除中的跳转
无论如何,编译器(我检查了GCC和Clang)为C++20默认比较生成的输出与它们为naiveEqual
生成的输出基本相同。无论出于何种原因,GCC选择使用jne
而不是je
进行跳转
这是缺少的编译器优化吗
假设在目标CPU上,一个总是比另一个快,这将是一个公平的结论。将此实现为单个64位比较时,可能会出现性能悬崖:
您中断存储以加载转发
如果结构中的32位数字通过单独的存储指令写入内存,然后使用64位加载指令快速从内存加载回(在存储命中L1$之前),则执行将暂停,直到存储提交到全局可见的缓存L1$。如果加载是与以前的32位存储匹配的32位加载,那么现代CPU将通过在存储到达缓存之前将存储值转发到加载指令来避免存储加载暂停。如果多个CPU访问内存(一个CPU以不同于其他CPU的顺序查看自己的存储),这将违反顺序一致性,但大多数现代CPU体系结构(甚至x86)都允许这样做。转发还允许更多的代码被完全推测性地执行,因为如果必须回滚执行,则没有其他CPU可以看到使用此CPU上加载值的代码被推测性执行的存储
如果希望使用64位操作而不希望使用此性能悬崖,则可能需要确保结构也始终作为单个64位数字写入。@M.a No.请参阅提供的链接中的程序集输出。return std::memcmp(&a,&b,sizeof(a))==0代码>?它生成与优化版本相同的程序集,更具表现力。@dyp:Wow,是的,并使用vpmovsxdq
/vmovmskpd
而不是仅使用vmovmskps
/cmp al,0xf
(由于pcmpeqd
输入中的高零点比较相等,因此将始终设置前2位)。甚至vpmovmskb
;低8位就是我们所需要的。当然,纯标量在这里显然更好,但是如果它在寻找类似a.x==b.x&&a.y!=b.y
,您可以使用clang的SIMD策略,只需使用不同的比较值,比如在低2位中使用0x1
,而不是0x3
。对于C++20return std::bit_cast(a)==std::bit_cast(b)
是memcpy
/memcmp
的类型安全版本,它生成相同的优化程序集,@BrettHale:这种推理是非常错误的。例如,x<10&&x>1
优化为sub/cmp/setbe(未签名低于或等于)GCC当然愿意考虑C抽象机所做的工作,特别是如果它能在没有任何指令的情况下完成所有任务(包括从分支源到无分支ASM的转换)。。一个答案甚至指出,如果你承诺将点对齐,GCC实际上会进行所需的优化。但是memcpy不假定对齐…因此optimizedEqual不会假定该点对齐过度。那么…为什么memcpy版本不需要对齐?编译器会看穿memcpy,因为它复制了unal已签名的结构到寄存器…这是一个缺少的编译器优化,对齐方式以某种方式推动了它吗?这是一个有趣的观察结果,但我不觉得它回答了“为什么?”为什么这些是有效的、琐碎的和有价值的
struct alignas(std::int64_t) Point {
std::int32_t x, y;
};