Różnica między „++i”, a „i++”

Lis 05
2010

cpp

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 😉

6 komentarzy w “Różnica między „++i”, a „i++””

  1. Piatkosia napisał/a:

    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ć.

  2. Melouber napisał/a:

    Może to ingerencja Bogów sprawia, że i++ jest wolniejsze?

  3. Zegarek- napisał/a:

    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).

  4. Gość napisał/a:

    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".

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *