Оба цикла бесконечны, но мы можем видеть, какой из них требует больше инструкций / ресурсов за одну итерацию.
Используя 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 ++ и инструкциями процессора, и этот уровень имеет важное значение для производительности.
- Следовательно, производительность не может быть оценена только с помощью исходного кода.
- Компилятор должен быть достаточно умен, чтобы оптимизировать такие тривиальные случаи. Программисты не должны тратить свое время на размышления о них в подавляющем большинстве случаев.