Как 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 вместе со сборкой он был преобразован в, но результат был не более ясен, чем предыдущий.
может кто-нибудь кратко объяснить:
- на gcc-S-O2 выход.
- если цикл оптимизирован, как я ожидал (одна сумма, а не много подводит)?
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
компиляторы имеют в своем распоряжении несколько инструментов, чтобы сделать код более эффективным или более"эффективным":
Если результат вычисления никогда не используется, код, который выполняет вычисление, может быть опущен (если вычисление действовало на
volatile
значения, эти значения все еще должны быть прочитаны, но результаты чтения могут быть проигнорированы). Если результаты вычислений, которые его кормили, не были использованы, код, который их выполняет, также может быть опущен. Если такое упущение делает код для обоих путей в условной ветви идентичным, условие может рассматриваться как неиспользуемое и опущенное. Это не повлияет на поведение (кроме времени выполнения) любой программы, которая не делает доступ к памяти за пределами границ или не вызывает то, что приложение L назвало бы "критическим неопределенным поведением".если компилятор определяет, что машинный код, который вычисляет значение, может давать результаты только в определенном диапазоне, он может опустить любое условное тесты, результаты которых можно предсказать на этой основе. Как и выше, это не повлияет на поведение, отличное от времени выполнения, если код не вызывает "критическое неопределенное поведение".
если компилятор определяет, что определенные входные данные будут вызывать любую форму неопределенного поведения с кодом в написанном виде, стандарт позволит компилятору опустить любой код, который будет иметь значение только при получении таких входных данных, даже если естественное поведение платформы выполнения задано такие входные данные были бы доброкачественными, и переписывание компилятора сделало бы его опасным.
хорошие компиляторы делают #1 и #2. По какой-то причине, однако, № 3 стал модным.