GCC如何优化循环内增加的未使用变量?

时间:2012-01-12 20:31:28

标签: c gcc compiler-optimization disassembly

我写了这个简单的C程序:

int main() {
    int i;
    int count = 0;
    for(i = 0; i < 2000000000; i++){
        count = count + 1;
    }
}

我想看看gcc编译器如何优化这个循环(显然添加 1 2000000000次应该是“add 2000000000 一次”)。所以:

gcc test.c 然后time上的a.out给出了:

real 0m7.717s  
user 0m7.710s  
sys 0m0.000s  

$ gcc -O2 test.c 然后time on a.out`给出:

real 0m0.003s  
user 0m0.000s  
sys 0m0.000s  

然后我用gcc -S反汇编了两个。第一个似乎很清楚:

    .file "test.c"  
    .text  
.globl main
    .type   main, @function  
main:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    movq    %rsp, %rbp
    .cfi_offset 6, -16
    .cfi_def_cfa_register 6
    movl    $0, -8(%rbp)
    movl    $0, -4(%rbp)
    jmp .L2
.L3:
    addl    $1, -8(%rbp)
    addl    $1, -4(%rbp)
.L2:
    cmpl    $1999999999, -4(%rbp)
    jle .L3
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2"
    .section    .note.GNU-stack,"",@progbits

L3添加,L2将-4(%rbp)1999999999进行比较,并在i < 2000000000时循环到L3。

现在优化的一个:

    .file "test.c"  
    .text
    .p2align 4,,15
.globl main
    .type main, @function
main:
.LFB0:
    .cfi_startproc
    rep
    ret
    .cfi_endproc
.LFE0:
    .size main, .-main
    .ident "GCC: (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2"
    .section .note.GNU-stack,"",@progbits

我根本无法理解那里发生了什么!我对装配知之甚少,但我期待像

这样的东西
addl $2000000000, -8(%rbp)

我甚至尝试使用 gcc -c -g -Wa,-a,-ad -O2 test.c 来查看C代码及其转换为的程序集,但结果是没有更清楚的是前一个。

有人可以简单解释一下:

  1. gcc -S -O2 输出。
  2. 如果循环按照我的预期进行优化(一个总和而不是多个总和)?

2 个答案:

答案 0 :(得分:73)

编译器甚至比这更聪明。 :)

实际上,它意识到你没有使用循环的结果。所以它完全取出了整个循环!

这称为Dead Code Elimination

更好的测试是打印结果:

#include <stdio.h>
int main(void) {
    int i; int count = 0;
    for(i = 0; i < 2000000000; i++){
        count = count + 1;
    }

    //  Print result to prevent Dead Code Elimination
    printf("%d\n", count);
}

编辑:我添加了必需的#include <stdio.h>; MSVC程序集列表对应于没有#include的版本,但它应该是相同的。


此刻我没有GCC在我面前,因为我已经启动进入Windows。但这是在MSVC上使用printf()的版本的反汇编:

编辑:我的汇编输出错误了。这是正确的。

; 57   : int main(){

$LN8:
    sub rsp, 40                 ; 00000028H

; 58   : 
; 59   : 
; 60   :     int i; int count = 0;
; 61   :     for(i = 0; i < 2000000000; i++){
; 62   :         count = count + 1;
; 63   :     }
; 64   : 
; 65   :     //  Print result to prevent Dead Code Elimination
; 66   :     printf("%d\n",count);

    lea rcx, OFFSET FLAT:??_C@_03PMGGPEJJ@?$CFd?6?$AA@
    mov edx, 2000000000             ; 77359400H
    call    QWORD PTR __imp_printf

; 67   : 
; 68   : 
; 69   : 
; 70   :
; 71   :     return 0;

    xor eax, eax

; 72   : }

    add rsp, 40                 ; 00000028H
    ret 0

是的,Visual Studio进行了这种优化。我认为GCC也可能会这样做。

是的,GCC执行类似的优化。这是使用gcc -S -O2 test.c(gcc 4.5.2,Ubuntu 11.10,x86)的同一程序的汇编列表:

        .file   "test.c"
        .section        .rodata.str1.1,"aMS",@progbits,1
.LC0:
        .string "%d\n"
        .text
        .p2align 4,,15
.globl main
        .type   main, @function
main:
        pushl   %ebp
        movl    %esp, %ebp
        andl    $-16, %esp
        subl    $16, %esp
        movl    $2000000000, 8(%esp)
        movl    $.LC0, 4(%esp)
        movl    $1, (%esp)
        call    __printf_chk
        leave
        ret
        .size   main, .-main
        .ident  "GCC: (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2"
        .section        .note.GNU-stack,"",@progbits

答案 1 :(得分:1)

编译器可以使用一些工具来提高代码效率或更高效率&#34;:

  1. 如果从未使用计算结果,则可以省略执行计算的代码(如果计算对volatile值起作用,则必须仍然读取这些值,但读取结果可能会被忽略)。如果没有使用提供它的计算结果,那么也可以省略执行这些计算的代码。如果这种省略使得条件分支上的两个路径的代码相同,则该条件可以被认为是未使用的并且被省略。这对任何没有超出内存访问权限的程序的行为(执行时间除外)都没有影响,或者调用附件L所称的内容&#34;关键未定义行为&#34;。< / p>

  2. 如果编译器确定计算值的机器代码只能生成某个范围内的结果,则可以省略任何条件测试,其结果可以在此基础上预测。如上所述,这不会影响执行时间以外的行为,除非代码调用&#34;关键未定义行为&#34;。

  3. 如果编译器确定某些输入将使用所写的代码调用任何形式的未定义行为,则标准将允许编译器省略任何仅在收到此类输入时才相关的代码,即使执行平台的自然行为,如果这样的输入是良性的,编译器的重写会使它变得危险。

  4. 好的编译器会做#1和#2。然而,出于某种原因,#3已成为时尚。