Czy istnieje różnica w wydajności między i ++ i ++ i w C ++?

352

Mamy pytanie, czy istnieje różnica w wydajności między Ci++ i ++i C ?

Jaka jest odpowiedź na C ++?

Mark Harrison
źródło
Dokonałem tagowania, ponieważ te dwa tagi są najłatwiejszym sposobem na znalezienie pytań tego rodzaju. Przejrzałem też inne, które nie miały spójnych tagów i dałem im spójne tagi.
George Stocker,
104
Czy istnieje różnica w wydajności między używaniem C ++ i ++ C?
new123456
2
Artykuł: Czy uzasadnione jest użycie operatora iteratora przyrostka prefiksu ++ it zamiast operatora postfiks it ++? - viva64.com/en/b/0093

Odpowiedzi:

426

[Streszczenie: Użyj, ++ijeśli nie masz konkretnego powodu do użycia i++.]

W przypadku C ++ odpowiedź jest nieco bardziej skomplikowana.

Jeśli ijest 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 ijest instancją klasy C ++, wówczas i++i ++iwywoł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ć tmpzmiennej i powiązanego z nią konstruktora kopii. Jeśli konstruktor kopiowania jest drogi, może to mieć znaczący wpływ na wydajność.

Mark Harrison
źródło
3
Kompilator może uniknąć drugiej kopii zwracającej tmp, alokując tmp w wywołującym za pośrednictwem NRVO, jak wspomniano w innym komentarzu.
Blaisorblade,
7
Czy kompilator nie może tego całkowicie uniknąć, jeśli wstawiony jest operator ++?
Eduard - Gabriel Munteanu
16
Tak, jeśli operator ++ jest wstawiony i tmp nigdy nie jest używany, można go usunąć, chyba że konstruktor lub niszczyciel obiektu tmp ma skutki uboczne.
Zan Lynx,
5
@kriss: różnica między C i C ++ polega na tym, że w C masz gwarancję, że operator zostanie wstawiony, i w tym momencie przyzwoity optymalizator będzie w stanie usunąć różnicę; zamiast tego w C ++ nie można zakładać wstawiania - nie zawsze.
Blaisorblade,
3
Dałbym +1, jeśli odpowiedź wspomniałaby coś o klasach, które przechowują wskaźniki (automatyczne, inteligentne lub prymitywne) do dynamicznie alokowanej pamięci (sterty), gdzie konstruktor kopii musi wykonywać głębokie kopie. W takich przypadkach nie ma argumentu, ++ i jest być może o rząd wielkości bardziej wydajny niż i ++. Kluczowe jest, aby przyzwyczaić się do stosowania wstępnej inkrementacji, gdy algorytm po inkrementacji nie jest faktycznie wymagany przez Twój algorytm, a następnie będziesz miał zwyczaj pisania kodu, który z natury nadaje się do większej wydajności, niezależnie od tego, jak Twój kompilator może zoptymalizować.
fonetagger
64

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.

wilhelmtell
źródło
2
„Operator wstępnej inkrementacji wprowadza zależność danych w kodzie: procesor musi poczekać na zakończenie operacji inkrementacji, zanim jego wartość będzie mogła zostać użyta w wyrażeniu. W przypadku głęboko potokowego procesora powoduje to przeciągnięcie. Brak zależności danych dla operatora przyrostowego. ” ( Game Engine Architecture (2nd edition) ) Więc jeśli kopia przyrostu postu nie jest intensywna obliczeniowo, nadal może przekroczyć przyrost wstępny.
Matthias
Jak to działa w kodzie Postfiksa C t(*this); ++(*this); return t;W drugim wierszu zwiększasz prawidłowo ten wskaźnik, więc jak tsię go aktualizuje, jeśli to zwiększasz. Czy wartości tego nie zostały już skopiowane t?
rasen58
The operator++(int) function must create a copy.nie, nie jest. Nie więcej kopii niżoperator++()
Severin Pappadeux,
47

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;
}

Przyrost O (n)

Test

// 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

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

Przyrost O (1)

Test

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

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

Wniosek

Pod względem wydajności

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.

Semantycznie

  • i++mówi increment i, I am interested in the previous value, though.
  • ++imówi increment i, I am interested in the current valuelub increment i, no interest in the previous value. Ponownie przyzwyczaisz się do tego, nawet jeśli nie jesteś teraz.

Knuth.

Przedwczesna optymalizacja jest źródłem wszelkiego zła. Podobnie jak przedwczesna pesymizacja.

fresnela
źródło
1
Ciekawy test. Teraz, prawie dwa i pół roku później, gcc 4.9 i Clang 3.4 wykazują podobny trend. Clang jest nieco szybszy w obu przypadkach, ale różnica między prefiksem i postfiksem jest gorsza niż gcc.
żuć skarpetki
To, co naprawdę chciałbym zobaczyć, to prawdziwy przykład, w którym ++ i / i ++ robi różnicę. Na przykład, czy to wpływa na którykolwiek z iteratorów standardu?
Jakob Schou Jensen
@JakobSchouJensen: Były to raczej przykłady prawdziwego świata. Rozważ dużą aplikację ze złożonymi strukturami drzew (np. Drzewa kd, drzewa quad) lub duże kontenery używane w szablonach ekspresji (w celu maksymalizacji przepustowości danych na sprzęcie SIMD). Jeśli robi to różnicę, nie jestem do końca pewien, dlaczego powróciłby do post-inkrementacji dla konkretnych przypadków, jeśli nie jest to potrzebne semantycznie.
Sebastian Mach
@phresnel: Nie sądzę, aby operator ++ był w twoim codziennym szablonie wyrażeń - czy masz tego faktyczny przykład? Typowe użycie operatora ++ jest na liczbach całkowitych i iteratorach. Myślę, że to byłoby interesujące wiedzieć, czy istnieje jakakolwiek różnica (oczywiście nie ma różnicy na liczbach całkowitych - ale iteratory).
Jakob Schou Jensen
@JakobSchouJensen: Brak rzeczywistego przykładu biznesowego, ale niektóre aplikacje, w których liczy się wiele rzeczy. Pisz iteratory, weź pod uwagę ray tracer napisany w idiomatycznym stylu C ++, a masz iterator do pierwszego przejścia w głąb, tak aby 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, indexi takie tam. Podsumowując, moje stanowisko jest takie, nawet jeśli istnieje tylko kilka przykładów ...
Sebastian Mach
20

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.

James Sutherland
źródło
8
Zapomniałeś zauważyć ważną kwestię, że tutaj wszystko jest podkreślone. Jeśli definicje operatorów nie są dostępne, nie można uniknąć kopiowania wykonanego w kodzie poza linią; z wbudowanym optymalizatorem jest dość oczywiste, więc zrobi to każdy kompilator.
Blaisorblade,
14

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.

martjno
źródło
1
„Decyzja: w przypadku prostych wartości skalarnych (niebędących obiektami) nie ma powodu, aby preferować jedną formę i zezwalamy na oba. W przypadku iteratorów i innych typów szablonów użyj wstępnego przyrostu”.
Nosredna
2
Ech ... i co to jest?
Sebastian Mach
Wspomniany link w odpowiedzi jest obecnie zepsuty
karol
4

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.


źródło
4

@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 forpę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 ++ijest 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 ++Cbycia bardziej logiczną nazwą.

Motti
źródło
4
@Motti: (żartuje) Nazwa C ++ jest logiczna, jeśli przypomnisz sobie, że Bjarne Stroustrup C ++ początkowo zakodował ją jako prekompilator generujący program C. Stąd C ++ zwrócił starą wartość C. Lub może być poprawa, że ​​C ++ jest nieco koncepcyjnie wadliwy od samego początku.
kriss
4
  1. ++ i - szybciej nie używa wartości zwracanej
  2. i ++ - szybciej przy użyciu wartości zwracanej

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

Hans Malherbe
źródło
3

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

0124816
źródło
3

Różnica w wydajności pomiędzy ++ii 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 inttak, jakby to było struct.

++iinkrementuje 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 structlub classjest 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.

DragonLord
źródło
1

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

Mike Dunlavey
źródło
1

@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

Mat Noguchi
źródło
1
To nie ma znaczenia. NRVO unika potrzeby kopiowania t w „CC :: operator ++ (int)” z powrotem do programu wywołującego, ale i ++ nadal kopiuje starą wartość ze stosu programu wywołującego. Bez NRVO i ++ tworzy 2 kopie, jedną do t, a drugą z powrotem do dzwoniącego.
Blaisorblade,
0

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.

Josh
źródło
3
Przepraszam, ale mi to przeszkadza. Kto powiedział, że to „dobry nawyk”, kiedy prawie nigdy nie ma znaczenia? Jeśli ludzie chcą włączyć to do swojej dyscypliny, to dobrze, ale odróżnijmy istotne powody od osobistych upodobań.
Mike Dunlavey
@MikeDunlavey ok, więc z której strony zwykle korzystasz, kiedy to nie ma znaczenia? xD to jedno lub drugie, prawda? post ++ (jeśli używasz go w ogólnym znaczeniu. zaktualizuj go, zwróć stary) jest całkowicie gorszy od ++ pre (zaktualizuj, zwróć), nigdy nie ma powodu, dla którego chciałbyś mieć mniejszą wydajność. w przypadku, gdy chcesz go później zaktualizować, programista w ogóle nie zrobi nawet post ++. kopiowanie bez marnowania czasu, gdy już go mamy. zaktualizuj go po jego użyciu. to kompilatory mają zdrowy rozsądek, jaki chcesz.
Kałuża
@Puddle: Kiedy to słyszę: „nigdy nie ma powodu, dla którego chciałbyś mieć mniejszą wydajność”. Wiem, że słyszę „głupiutko - głupiutki funt”. Musisz docenić związane z tym wielkości. Tylko jeśli to stanowi więcej niż 1% czasu, powinieneś to przemyśleć. Zwykle, jeśli myślisz o tym, masz miliony razy większe problemy, których nie rozważasz, a to sprawia, że ​​oprogramowanie jest znacznie wolniejsze niż mogłoby być.
Mike Dunlavey
@MikeDunlavey zwrócił bzdury, by zaspokoić swoje ego. próbujesz brzmieć jak jakiś mądry mnich, a jednak nic nie mówisz. zaangażowane wielkości ... jeśli tylko w ponad 1% przypadków powinieneś się tym przejmować ... xD absolutny drybling. jeśli jest nieefektywny, warto o tym wiedzieć i naprawić. zastanawiamy się nad tym właśnie z tego powodu! nie martwimy się, ile możemy zyskać na tej wiedzy. a kiedy powiedziałem, że nie chcesz mniejszej wydajności, śmiało wyjaśnij jeden cholerny scenariusz. MR WISE!
Kałuża
0

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

Geoffroy
źródło
To jest C, podczas gdy OP poprosił o C ++. W C jest tak samo. W C ++ szybszy jest ++ i; ze względu na swój przedmiot. Jednak niektóre kompilatory mogą zoptymalizować operatora post-increment.
Wiggler Jtag
0

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.

Blaisorblade
źródło
Widzę, że pracujesz nad doktoratem. z zainteresowaniem optymalizacją kompilatora i tym podobne. To świetnie, ale nie zapominaj, że akademia to komora echa, a zdrowy rozsądek często zostaje pozostawiony za drzwiami, przynajmniej w CS. Może Cię to zainteresować: stackoverflow.com/questions/1303899/...
Mike Dunlavey
Nigdy nie byłem ++ibardziej 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”
Sebastian Mach
strncpysł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.
MM
@MattMcNabb: czy 8 znaków nie zawiera nazwy MS-DOS? C został wynaleziony z Uniksem. W każdym razie, nawet jeśli strncpy miał rację, brak strlcpy nie był w pełni uzasadniony: nawet oryginalny C miał tablice, których nie powinieneś przepełniać, co wymagało strlcpy; najwyżej brakowało tylko atakujących zamierzających wykorzystać błędy. Ale nie można powiedzieć, że prognozowanie tego problemu było trywialne, więc jeśli przepisałem mój post, nie użyłbym tego samego tonu.
Blaisorblade,
@ Blaisorblade: Jak pamiętam, wczesne nazwy plików UNIX były ograniczone do 14 znaków. Brak strlcpy()uzasadniono faktem, że jeszcze go nie wynaleziono.
Keith Thompson,
-1

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.

Severin Pappadeux
źródło
-5

++ijest 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;
}

To samo dla C ++ :

#include <iostream>
using namespace std;

int main(){
    int a = 0;
    cout << a++;
    cout << ++a;
    return 0;
}
Cees Timmerman
źródło