В сырцах разных авторов видел я разные варианты вечного цикла. Чаще всего мне встречались следующие:
while (true) {
...
}
и
for (;;) {
...
}
Поскольку каждый защищал “свой вечный цикл” как родного, я решил разобраться. Кто же пишет более оптимальный код.
Я написал 2 исходника:
while.c:
#include <stdio.h>
int main (int argc, char* argv[])
{
while(1){
printf("1n");
}
}
for.c:
#include <stdio.h>
int main (int argc, char* argv[])
{
for(;;){
printf("1n");
}
}
Собрал их:
$ gcc -O3 while.c -o while.o3
$ gcc -O2 while.c -o while.o2
$ gcc -O1 while.c -o while.o1
$ gcc -O3 for.c -o for.o3
$ gcc -O2 for.c -o for.o2
$ gcc -O1 for.c -o for.o1
И дезассемблировал. Кому лень читать ассемблерные листниги — можете прокрутить страницу вниз. Собственно листинги:
$ objdump -d ./while.o3
...
0000000000400430 <main>:
400430: 48 83 ec 08 sub $0x8,%rsp
400434: 0f 1f 40 00 nopl 0x0(%rax)
400438: bf d4 05 40 00 mov $0x4005d4,%edi
40043d: e8 be ff ff ff callq 400400 <puts@plt>
400442: eb f4 jmp 400438 <main+0x8>
...
$ objdump -d ./while.o2
...
0000000000400430 <main>:
400430: 48 83 ec 08 sub $0x8,%rsp
400434: 0f 1f 40 00 nopl 0x0(%rax)
400438: bf d4 05 40 00 mov $0x4005d4,%edi
40043d: e8 be ff ff ff callq 400400 <puts@plt>
400442: eb f4 jmp 400438 <main+0x8>
...
$ objdump -d ./while.o1
...
000000000040051c <main>:
40051c: 48 83 ec 08 sub $0x8,%rsp
400520: bf d4 05 40 00 mov $0x4005d4,%edi
400525: e8 d6 fe ff ff callq 400400 <puts@plt>
40052a: eb f4 jmp 400520 <main+0x4>
...
$ objdump -d ./for.o1
...
000000000040051c <main>:
40051c: 48 83 ec 08 sub $0x8,%rsp
400520: bf d4 05 40 00 mov $0x4005d4,%edi
400525: e8 d6 fe ff ff callq 400400 <puts@plt>
40052a: eb f4 jmp 400520 <main+0x4>
...
$ objdump -d ./for.o2
...
0000000000400430 <main>:
400430: 48 83 ec 08 sub $0x8,%rsp
400434: 0f 1f 40 00 nopl 0x0(%rax)
400438: bf d4 05 40 00 mov $0x4005d4,%edi
40043d: e8 be ff ff ff callq 400400 <puts@plt>
400442: eb f4 jmp 400438 <main+0x8>
...
$ objdump -d ./for.o3
0000000000400430 <main>:
400430: 48 83 ec 08 sub $0x8,%rsp
400434: 0f 1f 40 00 nopl 0x0(%rax)
400438: bf d4 05 40 00 mov $0x4005d4,%edi
40043d: e8 be ff ff ff callq 400400 <puts@plt>
400442: eb f4 jmp 400438 <main+0x8>
Разбираем на пальцах
Различные оптимизации не повлияли на реализацию цикла while (true) — он всегда выполнял 3 команды: mov, callq и jmp. Так же оптимизации не повлияли на реализацию for — он тоже всегда был из 3х команд: mov, callq, jmp. Между собой mov, callq и jmp ничем не отличались. Длинна команд в байтах во всех 6и случаях неизменна.
Есть только небольшая разница между реализациями -O1 и -O2/-O3 jmp выполнялся на main+4 а не на main+8, но с учетом того, что это статичный адрес (как видно из asm-кода) оно тоже не несет разницы в производительности… Хотя… а вдруг страницы памяти разные, ведь на сколько я знаю для телодвижений между разными страницами памяти в x86 (и amd64) требуются дополнительные усилия проца!
Узнаем:
400438/4096 = 97,763183594
400520/4096 = 97,783203125
Пронесло. Страница памяти одна. Да это 97 страница Виртуальной памяти Виртуального адресного пространства процесса. Но именно она нам и нужна.
Итог
while (true) и for (;;) идентичны по производительности между собой и с любыми оптимизациями -Ox. Так что если Вас спросят кто из них быстрее — смело говорите что “for (;;)” — 8 символов написать быстрее, чем “while (true)” — 12 символов.
Для тех, кто не верит что без -Ox будет тоже самое:
$ gcc while.c -o while.noO
$ objdump -d while.noO
...
40052b: bf e4 05 40 00 mov $0x4005e4,%edi
400530: e8 cb fe ff ff callq 400400 <puts@plt>
400535: eb f4 jmp 40052b <main+0xf>
...
$ gcc for.c -o for.noO
$ objdump -d for.noO
...
40052b: bf e4 05 40 00 mov $0x4005e4,%edi
400530: e8 cb fe ff ff callq 400400 <puts@plt>
400535: eb f4 jmp 40052b <main+0xf>
...
P.S. конечно все это будет правдой на компиляторе “gcc version 4.7.2 (Debian 4.7.2-5)”
Автор: piromanlynx