Mamy pytanie, czy istnieje różnica w wydajności między Ci++
i ++i
C ?
Jaka jest odpowiedź na C ++?
Mamy pytanie, czy istnieje różnica w wydajności między Ci++
i ++i
C ?
Jaka jest odpowiedź na C ++?
Odpowiedzi:
[Streszczenie: Użyj, ++i
jeśli nie masz konkretnego powodu do użycia i++
.]
W przypadku C ++ odpowiedź jest nieco bardziej skomplikowana.
Jeśli i
jest to typ prosty (nie instancja klasy C ++), to odpowiedź podana dla C („Nie, nie ma różnicy w wydajności”) , ponieważ kompilator generuje kod.
Jeśli jednak i
jest instancją klasy C ++, wówczas i++
i ++i
wywołuje jedną z operator++
funkcji. Oto standardowa para tych funkcji:
Foo& Foo::operator++() // called for ++i
{
this->data += 1;
return *this;
}
Foo Foo::operator++(int ignored_dummy_value) // called for i++
{
Foo tmp(*this); // variable "tmp" cannot be optimized away by the compiler
++(*this);
return tmp;
}
Ponieważ kompilator nie generuje kodu, a jedynie wywołuje operator++
funkcję, nie można zoptymalizować tmp
zmiennej i powiązanego z nią konstruktora kopii. Jeśli konstruktor kopiowania jest drogi, może to mieć znaczący wpływ na wydajność.
Tak. Jest.
Operator ++ może, ale nie musi być zdefiniowany jako funkcja. W przypadku typów pierwotnych (int, double, ...) operatory są wbudowane, więc kompilator prawdopodobnie będzie w stanie zoptymalizować kod. Ale w przypadku obiektu, który definiuje operator ++, sytuacja wygląda inaczej.
Funkcja operator ++ (int) musi utworzyć kopię. Jest tak, ponieważ oczekuje się, że postfix ++ zwróci inną wartość niż posiada: musi zachować swoją wartość w zmiennej temp, zwiększyć swoją wartość i zwrócić temp. W przypadku operatora ++ (), przedrostek ++ nie ma potrzeby tworzenia kopii: obiekt może się zwiększyć, a następnie po prostu zwrócić.
Oto przykład tego:
struct C
{
C& operator++(); // prefix
C operator++(int); // postfix
private:
int i_;
};
C& C::operator++()
{
++i_;
return *this; // self, no copy created
}
C C::operator++(int ignored_dummy_value)
{
C t(*this);
++(*this);
return t; // return a copy
}
Za każdym razem, gdy wywołujesz operator ++ (int), musisz utworzyć kopię, a kompilator nic na to nie poradzi. Po uzyskaniu wyboru użyj operatora ++ (); w ten sposób nie zapisujesz kopii. Może to mieć znaczenie w przypadku wielu przyrostów (duża pętla?) I / lub dużych obiektów.
C t(*this); ++(*this); return t;
W drugim wierszu zwiększasz prawidłowo ten wskaźnik, więc jak t
się go aktualizuje, jeśli to zwiększasz. Czy wartości tego nie zostały już skopiowane t
?
The operator++(int) function must create a copy.
nie, nie jest. Nie więcej kopii niżoperator++()
Oto punkt odniesienia dla przypadku, gdy operatorzy przyrostowi znajdują się w różnych jednostkach tłumaczeniowych. Kompilator z g ++ 4.5.
Na razie zignoruj problemy ze stylem
// a.cc
#include <ctime>
#include <array>
class Something {
public:
Something& operator++();
Something operator++(int);
private:
std::array<int,PACKET_SIZE> data;
};
int main () {
Something s;
for (int i=0; i<1024*1024*30; ++i) ++s; // warm up
std::clock_t a = clock();
for (int i=0; i<1024*1024*30; ++i) ++s;
a = clock() - a;
for (int i=0; i<1024*1024*30; ++i) s++; // warm up
std::clock_t b = clock();
for (int i=0; i<1024*1024*30; ++i) s++;
b = clock() - b;
std::cout << "a=" << (a/double(CLOCKS_PER_SEC))
<< ", b=" << (b/double(CLOCKS_PER_SEC)) << '\n';
return 0;
}
// b.cc
#include <array>
class Something {
public:
Something& operator++();
Something operator++(int);
private:
std::array<int,PACKET_SIZE> data;
};
Something& Something::operator++()
{
for (auto it=data.begin(), end=data.end(); it!=end; ++it)
++*it;
return *this;
}
Something Something::operator++(int)
{
Something ret = *this;
++*this;
return ret;
}
Wyniki (czasy w sekundach) z g ++ 4.5 na maszynie wirtualnej:
Flags (--std=c++0x) ++i i++
-DPACKET_SIZE=50 -O1 1.70 2.39
-DPACKET_SIZE=50 -O3 0.59 1.00
-DPACKET_SIZE=500 -O1 10.51 13.28
-DPACKET_SIZE=500 -O3 4.28 6.82
Weźmy teraz następujący plik:
// c.cc
#include <array>
class Something {
public:
Something& operator++();
Something operator++(int);
private:
std::array<int,PACKET_SIZE> data;
};
Something& Something::operator++()
{
return *this;
}
Something Something::operator++(int)
{
Something ret = *this;
++*this;
return ret;
}
Przyrostu nie robi nic. To symuluje przypadek, gdy przyrost ma stałą złożoność.
Wyniki są teraz bardzo różne:
Flags (--std=c++0x) ++i i++
-DPACKET_SIZE=50 -O1 0.05 0.74
-DPACKET_SIZE=50 -O3 0.08 0.97
-DPACKET_SIZE=500 -O1 0.05 2.79
-DPACKET_SIZE=500 -O3 0.08 2.18
-DPACKET_SIZE=5000 -O3 0.07 21.90
Jeśli nie potrzebujesz poprzedniej wartości, nawyk korzystania ze wstępnego przyrostu. Zachowaj spójność nawet z wbudowanymi typami, przyzwyczaisz się do tego i nie ryzykujesz niepotrzebnej utraty wydajności, jeśli kiedykolwiek zastąpisz typ wbudowany niestandardowym typem.
i++
mówi increment i, I am interested in the previous value, though
.++i
mówi increment i, I am interested in the current value
lub increment i, no interest in the previous value
. Ponownie przyzwyczaisz się do tego, nawet jeśli nie jesteś teraz.Przedwczesna optymalizacja jest źródłem wszelkiego zła. Podobnie jak przedwczesna pesymizacja.
for (it=nearest(ray.origin); it!=end(); ++it) { if (auto i = intersect(ray, *it)) return i; }
nie wspominając o faktycznej strukturze drzewa (BSP, kd, Quadtree, Octree Grid itp.). Taka iterator musiałyby utrzymywać jakiś stan, np parent node
, child node
, index
i takie tam. Podsumowując, moje stanowisko jest takie, nawet jeśli istnieje tylko kilka przykładów ...
Nie jest całkowicie poprawne stwierdzenie, że kompilator nie może zoptymalizować tymczasowej kopii zmiennej w przypadku postfiksa. Szybki test z VC pokazuje, że przynajmniej może to zrobić w niektórych przypadkach.
W poniższym przykładzie wygenerowany kod jest identyczny na przykład dla prefiksu i postfiksu:
#include <stdio.h>
class Foo
{
public:
Foo() { myData=0; }
Foo(const Foo &rhs) { myData=rhs.myData; }
const Foo& operator++()
{
this->myData++;
return *this;
}
const Foo operator++(int)
{
Foo tmp(*this);
this->myData++;
return tmp;
}
int GetData() { return myData; }
private:
int myData;
};
int main(int argc, char* argv[])
{
Foo testFoo;
int count;
printf("Enter loop count: ");
scanf("%d", &count);
for(int i=0; i<count; i++)
{
testFoo++;
}
printf("Value: %d\n", testFoo.GetData());
}
Niezależnie od tego, czy wykonujesz testFoo ++, czy testFoo ++, nadal otrzymujesz ten sam wynikowy kod. W rzeczywistości, bez odczytu licznika od użytkownika, optymalizator sprowadził wszystko do stałej wartości. Więc to:
for(int i=0; i<10; i++)
{
testFoo++;
}
printf("Value: %d\n", testFoo.GetData());
Wynikało z tego:
00401000 push 0Ah
00401002 push offset string "Value: %d\n" (402104h)
00401007 call dword ptr [__imp__printf (4020A0h)]
Chociaż z pewnością jest tak, że wersja po poprawce może być wolniejsza, może się okazać, że optymalizator będzie wystarczająco dobry, aby pozbyć się tymczasowej kopii, jeśli jej nie używasz.
W Google C ++ styl przewodnik mówi:
Wstępne zwiększenie i wstępne
Użyj formy przedrostka (++ i) operatorów inkrementacji i dekrementacji z iteratorami i innymi obiektami szablonu.
Definicja: Kiedy zmienna jest zwiększana (++ i lub i ++) lub zmniejszana (--i lub i--), a wartość wyrażenia nie jest używana, należy zdecydować, czy zwiększyć wstępną (zmniejszenie) czy późniejszą (zmniejszenie).
Plusy: Gdy wartość zwracana jest ignorowana, forma „przed” (++ i) nigdy nie jest mniej wydajna niż forma „po” (i ++) i często jest bardziej wydajna. Wynika to z faktu, że post-inkrementacja (lub dekrementacja) wymaga wykonania kopii i, która jest wartością wyrażenia. Jeśli i jest iteratorem lub innym typem nieskalarnym, kopiowanie może być kosztowne. Ponieważ dwa typy przyrostu zachowują się tak samo, gdy wartość jest ignorowana, dlaczego nie zawsze po prostu zwiększyć wstępnie?
Wady: w C rozwinęła się tradycja używania przyrostu, gdy nie jest używana wartość wyrażenia, szczególnie w przypadku pętli. Niektórzy uważają, że post-inkrement jest łatwiejszy do odczytania, ponieważ „subject” (i) poprzedza „czasownik” (++), podobnie jak w języku angielskim.
Decyzja: W przypadku prostych wartości skalarnych (niebędących obiektami) nie ma powodu, aby preferować jedną formę i zezwalamy na jedno lub drugie. W przypadku iteratorów i innych typów szablonów użyj wstępnego przyrostu.
Chciałbym bardzo niedawno zwrócić uwagę na świetny post Andrew Koeniga na temat Code Talk.
http://dobbscodetalk.com/index.php?option=com_myblog&show=Efficiency-versus-intent.html&Itemid=29
W naszej firmie stosujemy również konwencję ++ iter dla spójności i wydajności, w stosownych przypadkach. Ale Andrew podnosi zbyt szczegółowe szczegóły dotyczące zamiarów w porównaniu do wyników. Są chwile, kiedy chcemy użyć iter ++ zamiast iter ++.
Więc najpierw zdecyduj o swoich zamiarach, a jeśli wstęp lub post nie ma znaczenia, to idź z pre, ponieważ przyniesie to pewne korzyści w zakresie wydajności, unikając tworzenia dodatkowego obiektu i rzucając go.
@Ketan
... podnosi przeoczony szczegół dotyczący zamiarów vs. wydajności. Są chwile, kiedy chcemy użyć iter ++ zamiast iter ++.
Oczywiście post i inkrementacja mają inną semantykę i jestem pewien, że wszyscy zgadzają się, że kiedy wynik jest wykorzystywany, powinieneś użyć odpowiedniego operatora. Myślę, że pytanie brzmi, co należy zrobić, gdy wynik jest odrzucany (jak w for
pętlach). Odpowiedź na to pytanie (IMHO) jest taka, że ponieważ względy dotyczące wydajności są w najlepszym razie nieistotne, powinieneś robić to, co jest bardziej naturalne. Dla mnie ++i
jest to bardziej naturalne, ale moje doświadczenie mówi mi, że jestem w mniejszości, a używanie i++
spowoduje dla większości mniej metalu osób czytających Twój kod.
W końcu to dlatego język nie jest nazywany „++C
”. [*]
[*] Wprowadź obowiązkową dyskusję na temat ++C
bycia bardziej logiczną nazwą.
Gdy nie używa się wartości zwracanej, kompilator ma gwarancję, że nie użyje wartości tymczasowej w przypadku ++ i . Nie ma gwarancji, że będzie szybszy, ale nie będzie wolniejszy.
Podczas używania wartości zwracanej i ++ pozwala procesorowi wepchnąć zarówno przyrost, jak i lewą stronę do potoku, ponieważ nie zależą one od siebie. ++ I może zablokować potok, ponieważ procesor nie może uruchomić lewej strony, dopóki operacja wstępnej inkrementacji nie zostanie przeprowadzona całkowicie. Ponownie przeciągnięcie rurociągu nie jest gwarantowane, ponieważ procesor może znaleźć inne przydatne rzeczy, w które można się przyczepić.
Mark: Chciałem tylko zaznaczyć, że operator ++ jest dobrym kandydatem do wstawienia, a jeśli kompilator zdecyduje się to zrobić, nadmiarowa kopia zostanie w większości przypadków wyeliminowana. (np. typy POD, którymi zwykle są iteratory).
To powiedziawszy, w większości przypadków nadal lepiej jest używać iter ++. :-)
Różnica w wydajności pomiędzy ++i
i i++
będzie bardziej widoczna, gdy pomyślisz o operatorach jako funkcjach zwracających wartość i sposobie ich implementacji. Aby łatwiej zrozumieć, co się dzieje, poniższe przykłady kodu będą używane int
tak, jakby to było struct
.
++i
inkrementuje zmienną, a następnie zwraca wynik. Można to zrobić w miejscu i przy minimalnym czasie procesora, w wielu przypadkach wymagając tylko jednego wiersza kodu:
int& int::operator++() {
return *this += 1;
}
Tego samego nie można jednak powiedzieć i++
.
Po inkrementacji, i++
często postrzegane jest jako zwracanie pierwotnej wartości przed inkrementacją. Jednak funkcja może zwrócić wynik dopiero po zakończeniu . W rezultacie konieczne staje się utworzenie kopii zmiennej zawierającej pierwotną wartość, zwiększenie zmiennej, a następnie zwrócenie kopii zawierającej pierwotną wartość:
int int::operator++(int& _Val) {
int _Original = _Val;
_Val += 1;
return _Original;
}
Gdy nie ma różnicy funkcjonalnej między wstępnym i późniejszym przyrostem, kompilator może przeprowadzić optymalizację tak, aby nie było żadnej różnicy między nimi. Jednakże, jeśli kompozyt typ danych, takich jak struct
lub class
jest zaangażowany, konstruktor kopia zostanie wywołana na post-przyrostu, a to nie będzie możliwe, aby wykonać tę optymalizację jeśli potrzebna jest głęboka kopia. W związku z tym wzrost wstępny jest na ogół szybszy i wymaga mniej pamięci niż przyrostowy.
@Mark: Usunąłem moją poprzednią odpowiedź, ponieważ była nieco przewrócona i sam zasłużyłem na ocenę negatywną. Myślę, że to dobre pytanie w tym sensie, że pyta o to, co myśli wielu ludzi.
Zazwyczaj odpowiedź jest taka, że ++ i jest szybszy niż i ++ i bez wątpienia tak jest, ale większe pytanie brzmi: „kiedy powinno cię to obchodzić?”
Jeśli ułamek czasu procesora spędzanego na zwiększaniu iteratorów wynosi mniej niż 10%, możesz się tym nie przejmować.
Jeśli ułamek czasu procesora spędzanego na zwiększaniu iteratorów jest większy niż 10%, możesz sprawdzić, które instrukcje wykonują tę iterację. Sprawdź, czy możesz po prostu zwiększać liczby całkowite zamiast używać iteratorów. Są szanse, że możesz i chociaż może to być w pewnym sensie mniej pożądane, szanse są całkiem dobre, zaoszczędzisz zasadniczo cały czas spędzony w tych iteratorach.
Widziałem przykład, w którym inkrementacja iteratora pochłaniała ponad 90% czasu. W takim przypadku zwiększenie liczby całkowitej skróciło czas wykonania w zasadzie o tę kwotę. (tzn. lepsze niż 10-krotne przyspieszenie)
@wilhelmtell
Kompilator może pominąć tymczasowe. Dokładnie z innego wątku:
Kompilator C ++ może eliminować pliki tymczasowe oparte na stosie, nawet jeśli spowoduje to zmianę zachowania programu. Łącze MSDN dla VC 8:
http://msdn.microsoft.com/en-us/library/ms364057(VS.80).aspx
Powodem, dla którego powinieneś używać ++ i nawet na wbudowanych typach, w których nie ma przewagi wydajnościowej, jest stworzenie dobrego nawyku dla siebie.
Oba są tak szybkie;) Jeśli chcesz, to jest to samo obliczenie dla procesora, tylko kolejność, w jakiej jest wykonywana, różni się.
Na przykład następujący kod:
#include <stdio.h>
int main()
{
int a = 0;
a++;
int b = 0;
++b;
return 0;
}
Utwórz następujący zespół:
0x0000000100000f24 <main+0>: push %rbp 0x0000000100000f25 <main+1>: mov %rsp,%rbp 0x0000000100000f28 <main+4>: movl $0x0,-0x4(%rbp) 0x0000000100000f2f <main+11>: incl -0x4(%rbp) 0x0000000100000f32 <main+14>: movl $0x0,-0x8(%rbp) 0x0000000100000f39 <main+21>: incl -0x8(%rbp) 0x0000000100000f3c <main+24>: mov $0x0,%eax 0x0000000100000f41 <main+29>: leaveq 0x0000000100000f42 <main+30>: retq
Widzisz, że dla ++ i b ++ jest to także mnemonik, więc jest to ta sama operacja;)
Zadane pytanie dotyczyło tego, kiedy wynik nie jest wykorzystany (wynika to z pytania dotyczącego C). Czy ktoś może to naprawić, skoro pytanie brzmi „społeczność wiki”?
W przypadku przedwczesnych optymalizacji Knuth jest często cytowany. Zgadza się. ale Donald Knuth nigdy nie obroniłby się tym okropnym kodem, który można zobaczyć w tych dniach. Widziałeś kiedyś a = b + c wśród liczb całkowitych Java (nie int)? Odpowiada to 3 konwersjom bokserskim / unboxingowym. Ważne jest unikanie takich rzeczy. A bezużyteczne pisanie i ++ zamiast ++ i jest tym samym błędem. EDYCJA: Jak ładnie ujmuje to Fresnel w komentarzu, można to podsumować jako „przedwczesna optymalizacja jest zła, podobnie jak przedwczesna pesymizacja”.
Nawet fakt, że ludzie są bardziej przyzwyczajeni do i ++, jest niefortunnym dziedzictwem C, spowodowanym błędem koncepcyjnym K&R (jeśli podążysz za zamierzonym argumentem, jest to logiczny wniosek; a obrona K&R, ponieważ są K&R, jest bez znaczenia, są świetnie, ale nie są świetni jako projektanci języków; istnieje niezliczona ilość błędów w projekcie C, od get () do strcpy (), po API strncpy () (powinien mieć API strlcpy () od pierwszego dnia) ).
Przy okazji, jestem jednym z tych, którzy nie są wystarczająco przyzwyczajeni do C ++, aby znaleźć ++ irytujące do czytania. Mimo to używam tego, ponieważ potwierdzam, że to prawda.
++i
bardziej irytujący niż i++
(w rzeczywistości uważam, że jest fajniejszy), ale reszta twojego posta otrzymuje moje pełne potwierdzenie. Może dodać punkt „przedwczesna optymalizacja jest zła, podobnie jak przedwczesna pesymizacja”
strncpy
służyły celowi w systemach plików, z których wówczas korzystali; nazwa pliku była 8-znakowym buforem i nie musiała być zakończona zerem. Nie można ich winić za to, że nie widzieli 40 lat w przyszłość ewolucji języka.
strlcpy()
uzasadniono faktem, że jeszcze go nie wynaleziono.
Czas na dostarczenie ludziom klejnotów mądrości;) - istnieje prosta sztuczka, aby przyrostek C ++ działał tak samo jak przyrostek przedrostka (wymyśliłem to dla siebie, ale zobaczyłem to również w kodzie innych ludzi, więc nie jestem sam).
Zasadniczo sztuczka polega na użyciu klasy pomocnika, aby odłożyć przyrost po powrocie, a RAII przychodzi na ratunek
#include <iostream>
class Data {
private: class DataIncrementer {
private: Data& _dref;
public: DataIncrementer(Data& d) : _dref(d) {}
public: ~DataIncrementer() {
++_dref;
}
};
private: int _data;
public: Data() : _data{0} {}
public: Data(int d) : _data{d} {}
public: Data(const Data& d) : _data{ d._data } {}
public: Data& operator=(const Data& d) {
_data = d._data;
return *this;
}
public: ~Data() {}
public: Data& operator++() { // prefix
++_data;
return *this;
}
public: Data operator++(int) { // postfix
DataIncrementer t(*this);
return *this;
}
public: operator int() {
return _data;
}
};
int
main() {
Data d(1);
std::cout << d << '\n';
std::cout << ++d << '\n';
std::cout << d++ << '\n';
std::cout << d << '\n';
return 0;
}
Wynaleziony jest dla jakiegoś ciężkiego niestandardowego kodu iteratorów i skraca czas działania. Koszt prefiksu vs postfiksa jest teraz jednym odniesieniem, a jeśli jest to operator niestandardowy wykonujący duże ruchy, prefiks i postfiks dają mi ten sam czas działania.
++i
jest szybszy niż i++
dlatego, że nie zwraca starej kopii wartości.
Jest również bardziej intuicyjny:
x = i++; // x contains the old value of i
y = ++i; // y contains the new value of i
Ten przykład C wypisuje „02” zamiast „12”, którego można się spodziewać:
#include <stdio.h>
int main(){
int a = 0;
printf("%d", a++);
printf("%d", ++a);
return 0;
}
#include <iostream>
using namespace std;
int main(){
int a = 0;
cout << a++;
cout << ++a;
return 0;
}