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.
#include <iostream>
using namespace std;
int main() {
int i=0;
while(i<10) {
cout << ++i << endl;
}
}
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.
#include <iostream>
using namespace std;
int main() {
for(int i=0;i<=10;++i) {
cout<<i<<endl;
}
}
Po zdekompilowaniu
(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.
Natomiast
#include <iostream>
using namespace std;
int main() {
for(int i=0;i<=10;i++) {
cout<<i<<endl;
}
}
W postaci kodu maszynowego ma postać
(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.
Jest różnica? NIE! Zatem czas wykonania w obu pętlach jest identyczny 😉
7 listopada 2010 dnia 18:37
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ć.
7 listopada 2010 dnia 18:39
Według asma nie ma różnicy :>
13 listopada 2010 dnia 20:52
Może to ingerencja Bogów sprawia, że i++ jest wolniejsze?
13 listopada 2010 dnia 21:55
No ale właśnie nie jest :>
17 listopada 2010 dnia 19:37
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).
14 stycznia 2012 dnia 16:23
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".