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 然后在 a.out 上使用 time 命令会得到:

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

$ gcc -O2 test.c然后运行time ona.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中,对-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. 如果循环像我期望的那样进行优化(一个求和而不是多个求和)。

20
顺便说一下,欢迎来到Stackoverflow!这是一个很好的第一个问题的例子。 :) - Mysticial
2个回答

73
编译器比那还要聪明。 :)
事实上,它意识到你没有使用循环的结果。因此,它完全删除了整个循环!
这被称为 死代码消除
更好的测试是打印结果:
#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

2
我现在感觉很蠢。没想到(额...不知道)有死代码消除这回事。我尝试了printf()和gcc,它们产生了相当相似的优化代码。谢谢你的回答! - Haile
11
别觉得自己蠢。如果你刚开始学习微基准测试,这种事情并不明显。这只是学习过程的一部分。 - Mysticial
了解编译器如何做出这种决策会很有趣。如果那个循环确实有必要,那该怎么办呢? - kechap
4
这是一个相当重要的编译器主题。基本思路是编译器会生成一个依赖图,进而可以用来证明/反驳某些计算是否有必要。那些被证明不必要的部分将被消除。 - Mysticial
10
循环的唯一作用是增加本地变量count的值。C规范规定函数外部不知道局部变量,编译器也知道该函数不使用结果,因此按照规范的规定,如果不计算count,程序不可能产生任何影响,因此优化器可以将其删除。另一方面,如果将count声明为全局变量,则编译器必须以不同方式处理它。 - Russell Borogove
我正在做类似的事情,但我的代码故意使用了循环的“输出”。我甚至使用了x=x+i并与argv混合。非优化版本循环5秒钟,但-O9可以瞬间完成。 - Nick

1

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

  1. 如果计算的结果从未被使用过,执行该计算的代码可以被省略(如果计算作用于 volatile 值,则仍必须读取这些值,但读取的结果可以被忽略)。如果馈送计算的结果没有被使用过,则执行这些计算的代码也可以被省略。如果这种省略使得条件分支上两个路径的代码相同,则该条件可以被视为未使用并被省略。这对于不进行越界内存访问或调用 Annex L 所称的“Critical Undefined Behaviors”的任何程序的行为(除了执行时间)都没有影响。

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

  3. 如果编译器确定某些输入将以编写的代码引发任何形式的未定义行为,则标准允许编译器省略任何只与接收到这种输入相关的代码,即使给定这样的输入时执行平台的自然行为是良性的,而编译器的重写会使其变得危险。

好的编译器会执行#1和#2。然而,由于某种原因,#3变得流行起来。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接