为什么有些C编译器会在奇怪的地方设置函数的返回值?
我在最近一次关于为什么有些C编译器会在奇怪的地方设置函数的返回值?,c,gcc,assembly,optimization,compilation,C,Gcc,Assembly,Optimization,Compilation,我在最近一次关于array[I++]vsarray[I]的假定速度的争论中写下了这个片段;i++ int array[10]; int main(){ int i=0; while(i < 10){ array[i] = 0; i++; } return 0; } 但是,它将xor放在第二个movat-O3之后 mov QWORD PTR array[rip], 0 mov QWORD PT
array[I++]
vsarray[I]的假定速度的争论中写下了这个片段;i++
int array[10];
int main(){
int i=0;
while(i < 10){
array[i] = 0;
i++;
}
return 0;
}
但是,它将xor放在第二个mov
at-O3之后
mov QWORD PTR array[rip], 0
mov QWORD PTR array[rip+8], 0
xor eax, eax
mov QWORD PTR array[rip+16], 0
mov QWORD PTR array[rip+24], 0
mov QWORD PTR array[rip+32], 0
ret
国际商会 icc通常将其放置在
-O1
:
push rsi
xor esi, esi
push 3
pop rdi
call __intel_new_feature_proc_init
stmxcsr DWORD PTR [rsp]
xor eax, eax
or DWORD PTR [rsp], 32832
ldmxcsr DWORD PTR [rsp]
..B1.2:
mov DWORD PTR [array+rax*4], 0
inc rax
cmp rax, 10
jl ..B1.2
xor eax, eax
pop rcx
ret
但是在一个奇怪的地方,在-O2
push rbp
mov rbp, rsp
and rsp, -128
sub rsp, 128
xor esi, esi
mov edi, 3
call __intel_new_feature_proc_init
stmxcsr DWORD PTR [rsp]
pxor xmm0, xmm0
xor eax, eax
or DWORD PTR [rsp], 32832
ldmxcsr DWORD PTR [rsp]
movdqu XMMWORD PTR array[rip], xmm0
movdqu XMMWORD PTR 16+array[rip], xmm0
mov DWORD PTR 32+array[rip], eax
mov DWORD PTR 36+array[rip], eax
mov rsp, rbp
pop rbp
ret
和-O3
and rsp, -128
sub rsp, 128
mov edi, 3
call __intel_new_proc_init
stmxcsr DWORD PTR [rsp]
xor eax, eax
or DWORD PTR [rsp], 32832
ldmxcsr DWORD PTR [rsp]
mov rsp, rbp
pop rbp
ret
叮当声 在所有优化级别上,只有叮当声将
xor
直接放在ret
前面:
xorps xmm0, xmm0
movaps xmmword ptr [rip + array+16], xmm0
movaps xmmword ptr [rip + array], xmm0
mov qword ptr [rip + array+32], 0
xor eax, eax
ret
由于GCC和ICC都是在更高的优化级别上这样做的,我认为一定有某种很好的理由 为什么有些编译器会这样做
当然,代码在语义上是相同的,编译器可以根据需要对其重新排序,但由于这只会在更高的优化级别上发生变化,因此这一定是由某种优化引起的。处理器设置特定值的位置取决于通过执行树的时刻,因此可以确定,该寄存器不再需要,也不会被外部世界更改 下面是一个不那么琐碎的例子:
处理器通过memset将eax归零,因为memset可以更改其值。这一时刻取决于对复杂树的解析,对人类来说可能不符合逻辑。由于未使用
eax
,编译器可以随时将寄存器归零,并按预期工作
您没有注意到的一件有趣的事情是icc
-O2
版本:
xor eax, eax
or DWORD PTR [rsp], 32832
ldmxcsr DWORD PTR [rsp]
movdqu XMMWORD PTR array[rip], xmm0
movdqu XMMWORD PTR 16+array[rip], xmm0
mov DWORD PTR 32+array[rip], eax ; set to 0 using the value of eax
mov DWORD PTR 36+array[rip], eax
请注意,eax
为返回值归零,但也用于将2个内存区域(最后2条指令)归零,这可能是因为使用eax
的指令比使用立即归零操作数的指令短
所以一石二鸟。不同的指令有不同的延迟。有时,由于几个原因,更改指令顺序可以加快代码的速度。例如: 如果某条指令需要几个周期才能完成,如果它在函数末尾,程序只需等待它完成。如果它在函数中的较早位置,则在该指令完成时可能会发生其他事情。这不太可能是这里的实际原因,不过,仔细想想,因为寄存器的异或(xor)是我认为的低延迟指令。不过,延迟取决于处理器 但是,将XOR放置在其中可能与分离放置它的mov指令有关 还有一些优化利用了现代处理器的优化功能,如流水线、分支预测(就我所见,这里的情况并非如此……)等。您需要对这些功能有相当深入的了解,才能理解优化器可以如何利用它们
你可能会发现信息丰富。它向我指出了一个我以前从未见过的资源,但它有很多您想要(或不想要:-)知道但又不敢问的信息:-)这些内存访问预计会消耗至少几个时钟周期。您可以在不更改代码功能的情况下移动xor。通过在其空闲后通过一次/一些内存访问将其拉回来,不会花费任何执行时间,它与外部访问并行(处理器完成xor并等待外部活动,而不仅仅是等待外部活动)。如果你把它放在一堆没有内存访问的指令中,它至少要花费一个时钟。您可能知道,使用xor vs mov immediate可以减少指令的大小,可能不需要花费时钟,但可以节省二进制文件中的空间。ghee whiz是一款很酷的优化,可以追溯到最初的8086,即使它最终并没有为您节省太多钱,它今天仍然在使用。Like。。为什么不呢?如果代码在语义上是正确的,那么只要代码是最优的,编译器就可以生成它所希望的奇怪的代码。可读性不是一个要求。好吧,如果它输出奇怪的代码,一定是有人写了它来做到这一点,大概是出于一个很好的理由。如果没有充分的理由,为什么要让你的生活更艰难,改变指令的顺序呢?有些算法的输出可能会说“place
xor ax,ax
在X行和Y行之间的某个地方”。然后,另一个算法将把这个输出放在X
之后,因为它从X到Y遍历行。另一个算法将把它放在Y之前,因为它在相反方向遍历它。(当然是非常抽象和简化的…)这可能与处理器的流水线有关。仔细阅读指令调度。你似乎误解了这个问题。我并不是在问为什么它要展开循环,我问为什么它要在函数中间设置函数的返回值。AppEnter回答谢谢,我没有看到这个,它肯定解释了为什么ICC早就设置EAX登记器,所以它可以在以后加载0。我很好奇为什么GCC会这么做。不确定,不是专家,但也许在同一指令序列的中间这样做可以更好地使用指令流水线(因为它们不工作在相同的数据上,寄存器和内存)。。总体思路实际上似乎正是本例的要点:xor
指令与内存访问混合在一起,内存访问使用不同的CPU资源,并且具有较高的延迟。因此,在这些指令的中间放置<代码> XOR <代码>保证它以零开销执行。我喜欢你解释的方式,也许比我更清楚一些。但是延迟的调度在OooE时代不是非常有用的。自xor甚至没有定义好的延迟,因为它不依赖任何东西。@harold我同意给定代码中可能没有性能差异。然而,当涉及到处理更多的代码时,产生良好混合输出的编译器是
xor eax, eax
or DWORD PTR [rsp], 32832
ldmxcsr DWORD PTR [rsp]
movdqu XMMWORD PTR array[rip], xmm0
movdqu XMMWORD PTR 16+array[rip], xmm0
mov DWORD PTR 32+array[rip], eax ; set to 0 using the value of eax
mov DWORD PTR 36+array[rip], eax