请注意:该问题与代码质量和改进代码的方法无关,对以下问题的(无效)意义也不运行时差异。 这是关于的内容,以及为什么编译器优化会降低性能。
以下代码计算了m
以下的斐波那契素数:
int main() {
unsigned int m = 500000000u;
unsigned int i = 0u;
unsigned int a = 1u;
unsigned int b = 1u;
unsigned int c = 1u;
unsigned int count = 0u;
while (a + b <= m) {
for (i = 2u; i < a + b; ++i) {
c = (a + b) % i;
if (c == 0u) {
i = a + b;
// break;
}
}
if (c != 0u) {
count = count + 1u;
}
a = a + b;
b = a - b;
}
return count; // Just to "output" (and thus use) count
}
如果使用g++.exe (Rev2, Built by MSYS2 project) 9.2.0
进行编译且没有优化(-O0
),则生成的二进制文件将在1.9秒内(在我的计算机上)执行。使用-O1
和-O3
分别需要3.3秒和1.7秒。
我试图通过查看汇编代码(godbolt.org)和相应的控制流图(hex-rays.com/products/ida)来理解所生成的二进制文件,但是我的汇编技巧并不足够
最里面的break
中的显式if
使-O1
代码再次快速:
if (c == 0u) {
i = a + b; // Not actually needed any more
break;
}
“内联”循环的进度表达式也是如此:
for (i = 2u; i < a + b; ) { // No ++i any more
c = (a + b) % i;
if (c == 0u) {
i = a + b;
++i;
} else {
++i;
}
}
哪个优化可以解释性能下降?
是否有可能根据C ++代码来解释是什么触发了优化(即,对GCC内部没有深入的了解)?
类似地,对于替代方案(附加观察结果)为何明显阻止流氓优化的说法,是否有高层解释?
答案 0 :(得分:2)
我相信这是由编译器在使用-O1
和-O2
时生成的替换分支的“条件移动”指令(CMOVEcc)造成的。
使用-O0
时,语句if (c == 0u)
被编译为跳转:
cmp DWORD PTR [rbp-16], 0
jne .L4
使用-O1
和-O2
:
test edx, edx
cmove ecx, esi
在-O3
产生跳转时(类似于-O0
)
test edx, edx
je .L5
There is a known bug in gcc,其中“使用条件移动而不是进行比较和分支导致速度慢将近2倍”
如 rodrigo 在其评论中建议的那样,使用标志-fno-if-conversion
告诉gcc不要用条件移动代替分支,从而防止出现此性能问题。
答案 1 :(得分:2)
我认为问题出在org.jboss.resteasy.resteasy-multipart-provider
上,指示编译器执行-fif-conversion
而不是CMOV
进行比较。并且TEST/JZ
在一般情况下并不出色。
我知道,反汇编中有两点受此标志影响:
首先,将第13行中的CMOV
编译为:
if (c == 0u) { i = a + b; }
第二,test edx,edx //edx is c
cmove ecx,esi //esi is (a + b), ecx is i
被编译为
if (c != 0u) { count = count + 1u; }
妙招!它是将cmp eax,0x1 //eax is c
sbb r8d,0xffffffff //r8d is count, but what???
减去-1
但带有进位,并且仅当count
小于c
时才设置进位,这是无符号的,表示1
。因此,如果0
为0,它将-1减去eax
,然后又减去1:它不变。如果count
不为0,则会减去eax
,从而使变量递增。
现在,这避免了分支,但是以错过明显的优化为代价,即如果-1
,您可以直接跳至下一个c == 0u
迭代。这个很简单,甚至可以在while
中完成。
答案 2 :(得分:2)
这里发挥的重要作用是循环传输的数据依赖性。
查看最内层循环的慢速变体的机器代码。我在这里显示-O2
程序集,-O1
的优化程度较差,但总体上具有相似的数据依赖性:
.L4:
xorl %edx, %edx
movl %esi, %eax
divl %ecx
testl %edx, %edx
cmove %esi, %ecx
addl $1, %ecx
cmpl %ecx, %esi
ja .L4
了解%ecx
中循环计数器的增量如何取决于上一条指令(cmov
),该指令又取决于除法的结果,而该结果又取决于除法的值循环计数器。
实际上,在计算%ecx
中的值时存在一连串的数据依赖关系,该值跨越整个循环,并且由于执行循环的时间占主导地位,因此计算该链的时间决定了程序的执行时间。
调整程序以计算除数将显示其执行434044698 div
指令。在我的情况下,将程序所占用的机器周期数除以该数字可得出26个周期,这与div
指令的延迟时间相加,加上链中其他指令的大约3或4个周期(该链为div
-test
-cmov
-add
)。
相反,-O3
代码没有这种依赖关系链,因此它成为吞吐量约束而不是 latency-bound :执行时间-O3
的变体由计算434044698独立的div
指令的时间确定。
最后,为您的问题提供具体答案:
1。哪个优化可以解释性能下降?
作为另一个答案,这是if转换在最初存在控制依赖项的情况下创建循环承载的数据依赖项。当控制依赖项对应于不可预测的分支时,控制依赖项也可能代价高昂,但是在这种情况下,分支易于预测。
2。
是否有可能根据C ++代码来解释是什么触发了优化的(即,对GCC的内部没有深入的了解)?也许您可以想象将代码转换为的优化
for (i = 2u; i < a + b; ++i) {
c = (a + b) % i;
i = (c != 0) ? i : a + b;
}
在CPU上对三进制运算符进行求值的地方,使得i
的新值在计算c
之前是未知的。
3。同样,对于替代方案(附加观察结果)为何明显阻止流氓优化的观点,是否有高层解释?
在这些变体中,代码不适合进行if转换,因此不会引入有问题的数据依赖性。