Обидві петлі нескінченні, але ми можемо побачити, яка з них потребує більше інструкцій / ресурсів за ітерацію.
Використовуючи gcc, я склав дві наступні програми для збирання з різними рівнями оптимізації:
int main(void) {
while(1) {}
return 0;
}
int main(void) {
while(2) {}
return 0;
}
Навіть не маючи оптимізацій ( -O0
), згенерована збірка була однаковою для обох програм . Тому різниці швидкостей між двома петлями немає.
Для довідки, ось створена збірка (використовуючи gcc main.c -S -masm=intel
прапор оптимізації):
З -O0
:
.file "main.c"
.intel_syntax noprefix
.def __main; .scl 2; .type 32; .endef
.text
.globl main
.def main; .scl 2; .type 32; .endef
.seh_proc main
main:
push rbp
.seh_pushreg rbp
mov rbp, rsp
.seh_setframe rbp, 0
sub rsp, 32
.seh_stackalloc 32
.seh_endprologue
call __main
.L2:
jmp .L2
.seh_endproc
.ident "GCC: (tdm64-2) 4.8.1"
З -O1
:
.file "main.c"
.intel_syntax noprefix
.def __main; .scl 2; .type 32; .endef
.text
.globl main
.def main; .scl 2; .type 32; .endef
.seh_proc main
main:
sub rsp, 40
.seh_stackalloc 40
.seh_endprologue
call __main
.L2:
jmp .L2
.seh_endproc
.ident "GCC: (tdm64-2) 4.8.1"
З -O2
і -O3
(той же вихід):
.file "main.c"
.intel_syntax noprefix
.def __main; .scl 2; .type 32; .endef
.section .text.startup,"x"
.p2align 4,,15
.globl main
.def main; .scl 2; .type 32; .endef
.seh_proc main
main:
sub rsp, 40
.seh_stackalloc 40
.seh_endprologue
call __main
.L2:
jmp .L2
.seh_endproc
.ident "GCC: (tdm64-2) 4.8.1"
Насправді збірка, що генерується для циклу, однакова для кожного рівня оптимізації:
.L2:
jmp .L2
.seh_endproc
.ident "GCC: (tdm64-2) 4.8.1"
Важливі біти:
.L2:
jmp .L2
Я не можу читати складання дуже добре, але це, очевидно, безумовний цикл. jmp
Інструкція беззастережно скидає програму назад на .L2
етикетці, навіть НЕ порівнюючи значення проти істини, і, звичайно , відразу ж робить це знову , поки програма не буде якимось - то чином закінчилася. Це безпосередньо відповідає коду C / C ++:
L2:
goto L2;
Редагувати:
Цікаво, що навіть без оптимізації , наступні цикли дають такий же вихід (безумовний jmp
) у збірці:
while(42) {}
while(1==1) {}
while(2==2) {}
while(4<7) {}
while(3==3 && 4==4) {}
while(8-9 < 0) {}
while(4.3 * 3e4 >= 2 << 6) {}
while(-0.1 + 02) {}
І навіть на мій подив:
#include<math.h>
while(sqrt(7)) {}
while(hypot(3,4)) {}
Речі стають трохи цікавішими за допомогою визначених користувачем функцій:
int x(void) {
return 1;
}
while(x()) {}
#include<math.h>
double x(void) {
return sqrt(7);
}
while(x()) {}
У -O0
цих двох прикладах насправді виклик x
та порівняння для кожної ітерації.
Перший приклад (повернення 1):
.L4:
call x
testl %eax, %eax
jne .L4
movl $0, %eax
addq $32, %rsp
popq %rbp
ret
.seh_endproc
.ident "GCC: (tdm64-2) 4.8.1"
Другий приклад (повернення sqrt(7)
):
.L4:
call x
xorpd %xmm1, %xmm1
ucomisd %xmm1, %xmm0
jp .L4
xorpd %xmm1, %xmm1
ucomisd %xmm1, %xmm0
jne .L4
movl $0, %eax
addq $32, %rsp
popq %rbp
ret
.seh_endproc
.ident "GCC: (tdm64-2) 4.8.1"
Однак -O1
вгорі і вище вони обидва виробляють таку ж збірку, що і попередні приклади (безумовне jmp
повернення до попередньої мітки).
TL; DR
Під GCC різні петлі складаються на однакові збірки. Компілятор оцінює постійні значення і не заважає виконувати фактичне порівняння.
Мораль історії полягає в наступному:
- Існує шар перекладу між вихідним кодом C ++ та інструкціями CPU, і цей шар має важливі наслідки для продуктивності.
- Отже, продуктивність не може бути оцінена лише переглядом вихідного коду.
- Компілятор повинен бути досить розумним, щоб оптимізувати такі тривіальні випадки. Програмісти не повинні витрачати свій час на роздуми про них у переважній більшості випадків.