Od jakiegoś czasu o uszy obijało mi się stwierdzenie, iż preinkrementacja (++i) jest szybsza od postinkrementacji (i++) stosowana w pętli for. Różnica między tymi dwoma sposobami dodawania „1” do zmiennej z pewnością istnieje, ale czy jest różnica w szybkości przetwarzania tego przez procesor?
Mikołaj – kolega programujący w wolnych chwilach w C++ udowodnił mi, że wyświetlając ++i oraz i++ otrzymujemy dwa różne wyniki.
[codesyntax lang=”cpp”]
#include <iostream> using namespace std; int main() { int i=0; while(i<10) { cout << ++i << endl; } }
[/codesyntax]
W rezultacie zwraca
1
2
3
4
5
6
7
8
9
10
Natomiast kod różniący się od poprzedniego post, zamiast preinkrementacją wyświetla liczby o innych wartościach.
0
1
2
3
4
5
6
7
8
9
Dlaczego tak się dzieje? Mikołaj wyjaśnia
Jak masz ++zmienna to najpierw doda 1 do „zmienna”, a jak masz zmienna++ to po użyciu „zmienna” doda 1 ;]
Jak natomiast wygląda sprawa z szybkością tych dwóch zapisów? Aby to sprawdzić, najprościej zdekompilować kod do postaci assemblera.
[codesyntax lang=”cpp”]
#include <iostream> using namespace std; int main() { for(int i=0;i<=10;++i) { cout<<i<<endl; } }
[/codesyntax]
Po zdekompilowaniu
[codesyntax lang=”asm”]
(gdb) Dump of assembler code for function main: 0x0000000000400864 <+0>: push %rbp 0x0000000000400865 <+1>: mov %rsp,%rbp 0x0000000000400868 <+4>: sub $0x10,%rsp 0x000000000040086c <+8>: movl $0x0,-0x4(%rbp) 0x0000000000400873 <+15>: jmp 0x400895 <main+49> 0x0000000000400875 <+17>: mov -0x4(%rbp),%eax 0x0000000000400878 <+20>: mov %eax,%esi 0x000000000040087a <+22>: mov $0x601060,%edi 0x000000000040087f <+27>: callq 0x4006f8 <_ZNSolsEi@plt> 0x0000000000400884 <+32>: mov $0x400758,%esi 0x0000000000400889 <+37>: mov %rax,%rdi 0x000000000040088c <+40>: callq 0x400748 <_ZNSolsEPFRSoS_E@plt> 0x0000000000400891 <+45>: addl $0x1,-0x4(%rbp) 0x0000000000400895 <+49>: cmpl $0xa,-0x4(%rbp) 0x0000000000400899 <+53>: setle %al 0x000000000040089c <+56>: test %al,%al 0x000000000040089e <+58>: jne 0x400875 <main+17> 0x00000000004008a0 <+60>: mov $0x0,%eax 0x00000000004008a5 <+65>: leaveq 0x00000000004008a6 <+66>: retq End of assembler dump.
[/codesyntax]
Natomiast
[codesyntax lang=”cpp”]
#include <iostream> using namespace std; int main() { for(int i=0;i<=10;i++) { cout<<i<<endl; } }
[/codesyntax]
W postaci kodu maszynowego ma postać
[codesyntax lang=”asm”]
(gdb) Dump of assembler code for function main: 0x0000000000400864 <+0>: push %rbp 0x0000000000400865 <+1>: mov %rsp,%rbp 0x0000000000400868 <+4>: sub $0x10,%rsp 0x000000000040086c <+8>: movl $0x0,-0x4(%rbp) 0x0000000000400873 <+15>: jmp 0x400895 <main+49> 0x0000000000400875 <+17>: mov -0x4(%rbp),%eax 0x0000000000400878 <+20>: mov %eax,%esi 0x000000000040087a <+22>: mov $0x601060,%edi 0x000000000040087f <+27>: callq 0x4006f8 <_ZNSolsEi@plt> 0x0000000000400884 <+32>: mov $0x400758,%esi 0x0000000000400889 <+37>: mov %rax,%rdi 0x000000000040088c <+40>: callq 0x400748 <_ZNSolsEPFRSoS_E@plt> 0x0000000000400891 <+45>: addl $0x1,-0x4(%rbp) 0x0000000000400895 <+49>: cmpl $0xa,-0x4(%rbp) 0x0000000000400899 <+53>: setle %al 0x000000000040089c <+56>: test %al,%al 0x000000000040089e <+58>: jne 0x400875 <main+17> 0x00000000004008a0 <+60>: mov $0x0,%eax 0x00000000004008a5 <+65>: leaveq 0x00000000004008a6 <+66>: retq End of assembler dump.
[/codesyntax]
Jest różnica? NIE! Zatem czas wykonania w obu pętlach jest identyczny 😉
Czas tak, ale więcej pamięci żre. Bo jest przy i++ tworzony obiekt tymczasowy. Przy jednej zmiennej no problem, ale jak masz milionelementową dynamiczną tablicę bardzo rozbudowanych struktur, różnica może boleć.
Według asma nie ma różnicy :>
Może to ingerencja Bogów sprawia, że i++ jest wolniejsze?
No ale właśnie nie jest :>
Mi się wydaję, że kompilator automatycznie przerobił w pętli for postinkrementację na preinkrementację, z tej racji, że użycie w niej tej pierwszej formy jest bez sensu.
Polecam sprawdzić jak zachowa się program z pierwszego listingu (ten z pętlą while).
Oj – odkop tragiczny – ale – chciałbym się wypowiedzieć w tej kwesti – to co mówi Zegarek możliwe, będzie prawdą. Używając w programie:
for(int i=0;i<=10;i++) {
cout<<i<<endl;
}
for(int i=0;i<=10;++i) {
cout<<i<<endl;
}
Nie ma różnicy w wyświetlaniu, jak i też w czasie (patrząc na dekompilacje do Asemblera).
Jeżeli program automatycznie konwenteruje i++ do ++i.
Myślę, że dekompilacja pentli while – z pierwszego listingu będzie najlepszym sposobem na wyjaśnienie tej "pseudo zagadki".