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

Posted

技术标签:

【中文标题】GCC 如何优化循环内递增的未使用变量?【英文标题】:How does GCC optimize out an unused variable incremented inside a loop? 【发布时间】:2012-02-09 03:25:19 【问题描述】:

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

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

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

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

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 相加,L2 比较 -4(%rbp)1999999999,如果 i &lt; 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 代码及其转换为的程序集,但结果是没有更清楚,前一个。

谁能简单解释一下:

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

【问题讨论】:

好问题顺便说一句,欢迎来到 ***!这是一个很好的第一个问题的好例子。 :) 【参考方案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);

编辑:我已经添加了所需的#include &lt;stdio.h&gt;; 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

【讨论】:

好吧,我现在觉得自己很笨。没想到(eew..不知道)关于死代码消除。我尝试了 printf() 和 gcc,它产生了完全相同的优化代码。谢谢你的回答! 别觉得自己笨。如果您只是进入微基准测试,这种事情根本不明显。这只是学习过程的一部分。 了解编译器如何做出这种决定会很有趣。如果出于某种原因确实需要该循环怎么办? @marcushatchenson 这是一个相当大的编译器主题。基本思想是编译器生成一个Dependency Graph,然后可以用来证明/反驳是否需要某些计算。然后消除被证明不需要的东西。 @marcushatchenson - 循环的唯一作用是增加 count,这是一个局部变量。 C 规范声明函数之外的任何内容都不知道本地的,并且编译器知道函数不会对结果做任何事情。根据规范的规则,如果不计算count,则不会对程序产生影响,因此允许优化器将其丢弃。另一方面,如果将 count 声明为全局变量,则编译器必须区别对待。【参考方案2】:

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

    如果从未使用过计算结果,则可以省略执行计算的代码(如果计算作用于 volatile 值,则仍必须读取这些值,但读取的结果可能是忽略)。如果没有使用提供给它的计算结果,那么执行这些计算的代码也可以省略。如果这样的省略使条件分支上的两条路径的代码相同,则该条件可以被视为未使用并被省略。这不会影响任何不进行越界内存访问或调用附件 L 称为“关键未定义行为”的程序的行为(执行时间除外)。

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

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

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

【讨论】:

以上是关于GCC 如何优化循环内递增的未使用变量?的主要内容,如果未能解决你的问题,请参考以下文章

SAS 对变量进行组内编号、循环编号、递增编号和有限重复循环编号

robotframework中,如何循环输出递增变量。 $menu_count set variable 10

对于循环:变量的递增和递减[关闭]

如何在 PL/SQL 中创建一个以时间段递增的循环

使用递增的变量在 c 中粘贴标记

减少我在 for 循环中递增的变量