Как gcc оптимизирует неиспользуемую переменную, увеличенную внутри цикла?

я написал эту простую программу на Си:

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

Я хотел посмотреть, как компилятор gcc оптимизирует этот цикл (ясно добавьте 1 2000000000 раз должно быть "add 2000000000 один раз"). Итак:

тест gcc.c а то time on a.out выдает:

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

$ Оук -O2 тест.c а то time onа.из` дает:

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

затем я разобрал оба с gcc -S. Первый одно кажется совершенно ясным:--14-->

    .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    , -8(%rbp)
    movl    , -4(%rbp)
    jmp .L2
.L3:
    addl    , -8(%rbp)
    addl    , -4(%rbp)
.L2:
    cmpl    99999999, -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 и петли для L3, если i < 2000000000.

теперь оптимизированный:

    .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 00000000, -8(%rbp)

Я даже пробовал с gcc-c-g-Wa,-a,-ad-O2 тест.c посмотреть код на C вместе со сборкой он был преобразован в, но результат был не более ясен, чем предыдущий.

может кто-нибудь кратко объяснить:

  1. на gcc-S-O2 выход.
  2. если цикл оптимизирован, как я ожидал (одна сумма, а не много подводит)?

2 ответов


компилятор даже умнее. :)

в самом деле, он понимает, что вы не используете результат цикла. Таким образом, он полностью уничтожил всю петлю!

это называется Исключение "Мертвых" Код.

лучшим тестом является печать результата:

#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, но это должно быть тот же.


у меня сейчас нет GCC передо мной, так как я загружаюсь в Windows. Но вот разборка версии с printf() на MSVC:

EDIT: у меня был неправильный вывод сборки. Вот правильным.

; 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    , %esp
        movl    00000000, 8(%esp)
        movl    $.LC0, 4(%esp)
        movl    , (%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. Если результат вычисления никогда не используется, код, который выполняет вычисление, может быть опущен (если вычисление действовало на volatile значения, эти значения все еще должны быть прочитаны, но результаты чтения могут быть проигнорированы). Если результаты вычислений, которые его кормили, не были использованы, код, который их выполняет, также может быть опущен. Если такое упущение делает код для обоих путей в условной ветви идентичным, условие может рассматриваться как неиспользуемое и опущенное. Это не повлияет на поведение (кроме времени выполнения) любой программы, которая не делает доступ к памяти за пределами границ или не вызывает то, что приложение L назвало бы "критическим неопределенным поведением".

  2. если компилятор определяет, что машинный код, который вычисляет значение, может давать результаты только в определенном диапазоне, он может опустить любое условное тесты, результаты которых можно предсказать на этой основе. Как и выше, это не повлияет на поведение, отличное от времени выполнения, если код не вызывает "критическое неопределенное поведение".

  3. если компилятор определяет, что определенные входные данные будут вызывать любую форму неопределенного поведения с кодом в написанном виде, стандарт позволит компилятору опустить любой код, который будет иметь значение только при получении таких входных данных, даже если естественное поведение платформы выполнения задано такие входные данные были бы доброкачественными, и переписывание компилятора сделало бы его опасным.

хорошие компиляторы делают #1 и #2. По какой-то причине, однако, № 3 стал модным.