C++ 为什么g++;将计算引入热循环?
我有一个非常奇怪的编译器行为,G++将计算拉入热循环,严重降低了生成代码的性能。这是怎么回事 考虑这一功能:C++ 为什么g++;将计算引入热循环?,c++,gcc,assembly,optimization,compiler-optimization,C++,Gcc,Assembly,Optimization,Compiler Optimization,我有一个非常奇怪的编译器行为,G++将计算拉入热循环,严重降低了生成代码的性能。这是怎么回事 考虑这一功能: #include <cstdint> constexpr bool noLambda = true; void funnyEval(const uint8_t* columnData, uint64_t dataOffset, uint64_t dictOffset, int32_t iter, int32_t limit, int32_t* writer,const i
#include <cstdint>
constexpr bool noLambda = true;
void funnyEval(const uint8_t* columnData, uint64_t dataOffset, uint64_t dictOffset, int32_t iter, int32_t limit, int32_t* writer,const int32_t* dictPtr2){
// Computation X1
const int32_t* dictPtr = reinterpret_cast<const int32_t*>(columnData + dictOffset);
// Computation X2
const uint16_t* data = (const uint16_t*)(columnData + dataOffset);
// 1. The less broken solution without lambda
if (noLambda) {
for (;iter != limit;++iter){
int32_t t=dictPtr[data[iter]];
*writer = t;
writer++;
}
}
// 2. The totally broken solution with lambda
else {
auto loop = [=](auto body) mutable { for (;iter != limit;++iter){ body(iter); } };
loop([=](unsigned index) mutable {
int32_t t=dictPtr[data[index]];
*writer = t;
writer++;
});
}
}
使用常规for循环(noLambda=true
)时,代码会更好,因为X2不再被拉入循环,但X1仍然是!:
通过将循环中的dictPtr
(计算X1)替换为dictPtr2
(一个参数),您可以尝试这确实是循环中的X1,指令将消失:
这就是我想要的循环。一个简单的循环,加载值并存储结果,而无需将随机计算带入其中
那么,这里发生了什么?将计算拉入热循环很少是一个好主意,但是G++在这里似乎这么认为。这让我付出了真正的代价。兰姆达事件加剧了整个局势;这导致G++将更多的计算引入到循环中
这个问题如此严重,是因为这是一个非常简单的C++代码而没有华丽的特性。如果我不能依靠我的编译器为这样一个微不足道的例子生成完美的代码,我将需要检查代码中所有热循环的汇编,以确保一切尽可能快这也意味着可能有大量程序受此影响。您正在使用无符号32位类型作为数组索引(第21行)。这迫使编译器在循环的每个步骤中考虑是否溢出了可用的范围,在这种情况下,它需要返回到数组的开头。您看到的额外代码与此检查相关!至少有三种方法可以避免编译器采用这种过于谨慎的方法:
因此,概括一下:不是X1和X2的计算移动到循环中导致大小膨胀,而是使用了类型错误的数组索引变量 我试着运行你的代码,然后。。。令人惊讶的是:在循环中执行的指令并不是您在发布的编译器资源管理器链接中看到的指令。检查这个(我添加了一个主函数) 在循环中执行的指令是162-167,即
.L15:
movzwl 25(%rbx,%rdx), %ecx
movl 5(%rbx,%rcx,4), %ecx
movl %ecx, 0(%rbp,%rdx,2)
addq $2, %rdx
cmpq $180, %rdx
jne .L15
您可以通过在您的计算机上编译来再次检查这一点
g++ test.cpp -std=c++1z -g -O3
和gdb一起运行
> gdb a.out
(gdb) break funnyEval
(gdb) layout split #shows assebly
(gdb) stepi #steps to the next instruction
编译器生成funnyEval的不同非内联版本(即您在反汇编输出中看到的版本),即使实际使用的版本是内联的。我不知道(目前)这两者为何不同,但我想如果您受到性能损失的影响,您可以通过确保funnyEval内联来修复它:在头文件中定义,或者通过编译并链接链接时间优化(-flto)。我将尝试看看funnyEval在另一个翻译单元中时会发生什么…恭喜,您发现了一个gcc错误。主要的解决方案是用“missed optimization”关键字报告它。您的MCVE已经是该bug的优秀测试用例,因此编写一个测试用例不会花费太长时间。复制/粘贴代码和一些说明。一个链接到这个问答,一个链接到上的代码,也会很好 有时使用“额外”指令有微妙的微体系结构原因,如
xor
-将popcnt
/lzcnt
或bsf
的目标归零,但这里的情况并非如此。这很糟糕;循环内的movl%ecx,%eax
可能是使用比指针更窄的无符号类型的结果,但即使这样也可以更有效地完成;这也是一个遗漏的优化
我还没有看过GCC的GIMPLE或RTL转储(内部表示)来了解更多细节。计算值的唯一用途是在循环内部,因此我可以想象编译器对程序逻辑的内部表示可能在转换时丢失了循环内部/外部之间的差异。通常情况下,不需要在回路中的东西会从回路中提升或下沉
但不幸的是,gcc在循环内留下一条额外的mov
指令来设置循环外的代码并不罕见。特别是当可能需要多条指令在循环之外才能获得相同的效果时。当优化性能而不是代码大小时,这通常是一个糟糕的权衡。我并没有像我所希望的那样详细地研究概要文件引导优化的asm输出,以查看gcc知道哪些循环是真正热门的并展开它们的代码。但不幸的是,大多数代码都是在没有PGO的情况下生成的,因此不使用-fprofile的代码gen仍然非常重要
然而,这个问题的核心不是如何尽快得到这个特定的例子。相反,我对编译器如何在如此简单的snipp中产生这样的去优化感到相当后悔
.L15:
movzwl 25(%rbx,%rdx), %ecx
movl 5(%rbx,%rcx,4), %ecx
movl %ecx, 0(%rbp,%rdx,2)
addq $2, %rdx
cmpq $180, %rdx
jne .L15
g++ test.cpp -std=c++1z -g -O3
> gdb a.out
(gdb) break funnyEval
(gdb) layout split #shows assebly
(gdb) stepi #steps to the next instruction
# gcc7.2: the nolamba side of the if, with no actual if()
.L3:
movzwl (%rsi,%rax,2), %ecx
movl (%rdx,%rcx,4), %ecx
movl %ecx, (%r9,%rax,4) # indexed store: no port 7
addq $1, %rax # gcc8 -O3 -march=skylake uses inc to save a code byte here.
cmpq %rax, %r8
jne .L3