首页 文章

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

提问于
浏览
64

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

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

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

gcc test.c 然后 timea.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 .

Now the optimized one:

.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代码以及它转换为的程序集,但结果不再清楚前一个 .

Can someone briefly explain:

  • gcc -S -O2 输出 .

  • 如果循环按照我的预期进行优化(一个总和而不是多个总和)?

2 回答

  • 1

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

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

    这叫做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);
    }
    

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


    我没有't have GCC in front of me at the moment, since I' m启动到Windows . 但这是在MSVC上使用 printf() 对版本进行反汇编:

    EDIT : I had the wrong assembly output. Here's the correct one.

    ; 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
    
  • 73

    编译器有一些工具可以使代码更高效或更“高效”:

    • 如果从未使用计算结果,则可以省略执行计算的代码(如果计算作用于 volatile 值,则仍必须读取这些值,但可以忽略读取的结果) . 如果提供它的计算结果不是超出内存访问或调用附件L称之为"Critical Undefined Behaviors"的内容 .

    • 如果编译器确定计算值的机器代码只能生成某个范围内的结果,则可以省略任何条件测试,其结果可以在此基础上预测 . 如上所述,除非代码调用“Critical Undefined Behaviors”,否则这不会影响执行时间以外的行为 .

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

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

相关问题