为什么有些C编译器会在奇怪的地方设置函数的返回值?

时间:2017-08-10 20:03:15

标签: c gcc assembly optimization compilation

我在最近关于array[i++] vs array[i]; i++的假定速度的争论中写了这个片段。

int array[10];

int main(){
    int i=0;
    while(i < 10){
        array[i] = 0;
        i++;
    }
    return 0;
}

编译器资源管理器中的代码段:https://godbolt.org/g/de7TY2

正如预期的那样,编译器输出array[i++]array[i]; i++的asm相同,且至少为-O1。然而令我感到惊讶的是xor eax, eax似乎随机地放置在更高优化级别的函数中。

GCC

-O2,GCC按预期将xor放在ret之前

    mov     DWORD PTR [rax], 0
    add     rax, 4
    cmp     rax, OFFSET FLAT:array+40
    jne     .L2
    xor     eax, eax
    ret

但是它会在第二个mov之后将xor放在-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

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   

只有clang会将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都在更高的优化级别上这样做,我认为必定有某种充分的理由。

为什么有些编译器会这样做?

代码在语义上是相同的,编译器可以按照自己的意愿对其进行重新排序,但由于这只会在更高的优化级别进行更改,因此必须由某种优化引起。

4 个答案:

答案 0 :(得分:5)

由于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的指令短于具有立即数零的指令操作数。

所以两只一石二鸟。

答案 1 :(得分:5)

不同的指令有不同的延迟。有时,更改指令的顺序可以加速代码,原因有几个。例如: 如果某个指令需要几个周期才能完成,如果它在函数的末尾,程序就会等待它完成。如果它在函数的早期,则在该指令完成时可能发生其他事情。不过,这里的实际原因不太可能是第二个想法,因为我认为寄存器是低延迟指令。但是,延迟是依赖于处理器的。

但是,放置XOR可能与分离放置它的mov指令有关。

还有一些优化可以利用现代处理器的优化功能,例如流水线,分支预测(据我所知,这里不是这种情况......)等等。您需要对这些处理器有一个非常深刻的理解能够理解优化器可以利用它们做些什么。

您可能会发现this信息丰富。它向我指出了一个我以前从未见过的资源,但有很多你想要的信息(或者不想要:-))知道但是却不敢问:-) here

答案 2 :(得分:3)

预计这些存储器访问将至少燃烧几个时钟周期。您可以在不更改代码功能的情况下移动xor。通过一次/一些内存访问将其拉回来它就变得空闲,不花费你任何执行时间它与外部访问并行(处理器完成xor并等待外部活动而不是等待外部活动) 。如果你把它放在一堆没有内存访问的指令中,它至少要花费一个时钟。正如您可能知道使用xor vs mov立即减小指令的大小,可能不会花费时钟,但会节省二进制文件中的空间。一个非常酷的优化,可以追溯到最初的8086,并且至今仍然使用它,即使它最终没有为你节省太多。

答案 3 :(得分:-1)

在处理器设置的情况下,特定值取决于通过执行树的时刻,确保不再需要该寄存器,并且不会被外部世界更改。

这里不那么简单的例子: https://godbolt.org/g/6AowMJ

并且处理器将eax超过memset,因为memset可以更改其值。这个时刻取决于解析复杂的树,对人类来说可能不合逻辑。