Czy x + = szybciej niż x = x + a?

84

Czytałem „Język programowania C ++” Stroustrupa, w którym mówi, że na dwa sposoby można dodać coś do zmiennej

x = x + a;

i

x += a;

Woli, +=bo najprawdopodobniej jest lepiej zaimplementowany. Myślę, że ma na myśli, że to też działa szybciej.
Ale czy to naprawdę? Jeśli to zależy od kompilatora i innych rzeczy, jak mam to sprawdzić?

Chiffa
źródło
45
„Język programowania C ++” został opublikowany po raz pierwszy w 1985 roku. Najnowsza wersja została opublikowana w 1997 roku, a specjalne wydanie wersji z 1997 roku zostało opublikowane w 2000 roku. W konsekwencji niektóre części są bardzo nieaktualne.
JoeG
5
Te dwie linie mogłyby potencjalnie zrobić coś zupełnie innego. Musisz być bardziej szczegółowy.
Kerrek SB,
26
Nowoczesne kompilatory są wystarczająco inteligentne, aby można było uznać te pytania za „nieaktualne”.
gd1
2
Ponownie otworzyłem to, ponieważ zduplikowane pytanie dotyczy C, a nie C ++.
Kev

Odpowiedzi:

212

Wszelkie warta jego sól kompilator wygeneruje dokładnie taką samą sekwencję maszynowego języka dla obu konstruktów dla dowolnego typu wbudowanego ( int, float, etc), o ile oświadczenie naprawdę jest tak proste, jak x = x + a; i optymalizacji jest włączona . (Warto zauważyć, że GCC -O0, który jest trybem domyślnym, wykonuje antyoptymalizacje , takie jak wstawianie całkowicie niepotrzebnych magazynów do pamięci, aby upewnić się, że debuggery zawsze mogą znaleźć wartości zmiennych).

Jeśli jednak stwierdzenie jest bardziej skomplikowane, mogą być inne. Załóżmy fzatem, że jest to funkcja, która zwraca wskaźnik

*f() += a;

dzwoni ftylko raz, a tymczasem

*f() = *f() + a;

nazywa to dwukrotnie. Jeśli fma skutki uboczne, jeden z dwóch będzie zły (prawdopodobnie ten drugi). Nawet jeśli fnie ma skutków ubocznych, kompilator może nie być w stanie wyeliminować drugiego wywołania, więc to drugie może być rzeczywiście wolniejsze.

A ponieważ mówimy tutaj o C ++, sytuacja jest zupełnie inna w przypadku typów klas, które przeciążają operator+i operator+=. Jeśli xjest takim typem, to - przed optymalizacją - x += aprzekłada się na

x.operator+=(a);

podczas gdy x = x + aprzekłada się na

auto TEMP(x.operator+(a));
x.operator=(TEMP);

Teraz, jeśli klasa jest poprawnie napisana, a optymalizator kompilatora jest wystarczająco dobry, oba kończą się generowaniem tego samego języka maszynowego, ale nie jest to takie pewne, jak w przypadku typów wbudowanych. Prawdopodobnie o tym myśli Stroustrup, zachęcając do używania +=.

zwol
źródło
14
Jest jeszcze jeden aspekt - czytelność . Idiom C ++ do dodawania exprdo varis var+=expri zapisywania go w inny sposób zmyli czytelników.
Tadeusz Kopeć
21
Jeśli zauważysz, że piszesz, *f() = *f() + a;możesz dobrze przyjrzeć się temu, co naprawdę próbujesz osiągnąć ...
Adam Davis
3
A jeśli var = var + expr myli Cię, ale var + = expr nie, jesteś najdziwniejszym inżynierem oprogramowania, jakiego kiedykolwiek spotkałem. Albo są czytelne; po prostu upewnij się, że jesteś konsekwentny (i wszyscy używamy op =, więc i tak jest to dyskusja = P)
WhozCraig
7
@PiotrDobrogost: Co jest złego w odpowiadaniu na pytania? W każdym razie to pytający powinien sprawdzać, czy nie ma duplikatów.
Gorpik,
4
@PiotrDobrogost wydaje mi się, że jesteś trochę ... zazdrosny ... Jeśli chcesz poszukać duplikatów, zrób to. Ja, na przykład, wolę odpowiadać na pytania niż szukać naiwniaków (chyba że jest to pytanie, które szczególnie przypominam sobie, że widziałem je wcześniej). Czasami może to być szybsze, a więc szybciej pomóc temu, kto zadał pytanie. Zwróć też uwagę, że to nawet nie jest pętla. 1jest stałą, amoże być zmienną, typem zdefiniowanym przez użytkownika lub czymkolwiek. Zupełnie inny. W rzeczywistości nie rozumiem, jak to się w ogóle zamknęło.
Luchian Grigore
56

Możesz to sprawdzić patrząc na demontaż, który będzie taki sam.

W przypadku podstawowych typów oba są równie szybkie.

Są to dane wyjściowe generowane przez kompilację debugowania (tj. Brak optymalizacji):

    a += x;
010813BC  mov         eax,dword ptr [a]  
010813BF  add         eax,dword ptr [x]  
010813C2  mov         dword ptr [a],eax  
    a = a + x;
010813C5  mov         eax,dword ptr [a]  
010813C8  add         eax,dword ptr [x]  
010813CB  mov         dword ptr [a],eax  

W przypadku typów zdefiniowanych przez użytkownika , w których można przeciążać operator +i operator +=, zależy to od ich odpowiednich implementacji.

Luchian Grigore
źródło
1
Nie we wszystkich przypadkach. Zauważyłem, że szybsze może być ładowanie adresu pamięci do rejestru, inkrementowanie go i zapisywanie z powrotem niż bezpośrednie zwiększanie lokalizacji pamięci (bez użycia atomów). Spróbuję podszepnąć kod ...
James,
A co z typami zdefiniowanymi przez użytkownika? Dobre kompilatory powinny wygenerować równoważny zestaw, ale nie ma takiej gwarancji.
mfontanini
1
@LuchianGrigore Nie, jeśli awynosi 1, xto volatilekompilator może generować inc DWORD PTR [x]. To jest powolne.
James
1
@Chiffa nie zależy od kompilatora, ale zależy od programisty. Możesz zaimplementować, operator +aby nic nie robić i operator +=obliczyć 100000 liczbę pierwszą, a następnie zwrócić. Oczywiście byłoby to głupie, ale jest to możliwe.
Luchian Grigore
3
@James: jeśli program jest rozsądny różnicy pomiędzy wydajnością ++xi temp = x + 1; x = temp;, wtedy najprawdopodobniej powinna być napisana montaż zamiast C ++ ...
EmirCalabuch
11

Tak! Pisanie jest szybsze, szybsze do czytania i szybsze do zrozumienia, jeśli chodzi o to drugie w przypadku, gdy xmogą mieć skutki uboczne. Więc ogólnie jest to szybsze dla ludzi. Ogólnie rzecz biorąc, czas ludzki kosztuje znacznie więcej niż czas komputera, więc pewnie o to pytałeś. Dobrze?

Mark Adler
źródło
8

To naprawdę zależy od typu x i a oraz implementacji +. Dla

   T x, a;
   ....
   x = x + a;

kompilator musi utworzyć tymczasową wartość T, aby zawierała wartość x + a podczas jej oceny, którą następnie może przypisać do x. (Nie może używać x ani a jako obszaru roboczego podczas tej operacji).

Dla x + = a nie potrzeba tymczasowego.

W przypadku trywialnych typów nie ma różnicy.

Tom Tanner
źródło
8

Różnica między x = x + ai x += ato ilość pracy, jaką musi wykonać maszyna - niektóre kompilatory mogą (i zwykle to robią) optymalizować ją, ale zazwyczaj, jeśli przez jakiś czas zignorujemy optymalizację, dzieje się tak, że w poprzednim fragmencie kodu maszyna musi sprawdzać wartość xdwukrotnie, podczas gdy w drugim przypadku to wyszukiwanie musi nastąpić tylko raz.

Jednak, jak wspomniałem, obecnie większość kompilatorów jest wystarczająco inteligentna, aby przeanalizować instrukcje i zredukować wymagane instrukcje maszynowe.

PS: Pierwsza odpowiedź na temat przepełnienia stosu!

Sagar Ahire
źródło
6

Ponieważ oznaczyłeś to C ++, nie ma sposobu, aby dowiedzieć się z dwóch opublikowanych instrukcji. Musisz wiedzieć, co to jest „x” (to trochę jak odpowiedź „42”). Jeśli xjest to POD, to tak naprawdę nie będzie miało większego znaczenia. Jeśli jednak xjest to klasa, mogą występować przeciążenia metod operator +i operator +=, które mogą mieć różne zachowania, które prowadzą do bardzo różnych czasów wykonania.

Skizz
źródło
6

Jeśli powiesz +=, że znacznie ułatwiasz życie kompilatorowi. Aby kompilator rozpoznał, że x = x+ajest to to samo x += a, co kompilator musi

  • przeanalizuj lewą stronę ( x), aby upewnić się, że nie ma skutków ubocznych i zawsze odnosi się do tej samej wartości l. Na przykład mogłoby tak być z[i]i musi się upewnić, że jedno zi drugie inie ulegnie zmianie.

  • przeanalizuj prawą stronę ( x+a) i upewnij się, że jest to sumowanie i że lewa strona występuje raz i tylko raz po prawej stronie, mimo że można ją przekształcić, jak w z[i] = a + *(z+2*0+i).

Jeśli to, co masz na myśli to, aby dodać ado xpisarz kompilator docenia go po prostu powiedzieć, co masz na myśli. W ten sposób nie ćwiczysz tej części kompilatora, z której jego autor ma nadzieję, że wyciągnął wszystkie błędy, a to w rzeczywistości nie ułatwia ci życia, chyba że szczerze nie możesz się wyrwać trybu Fortran.

Mike Dunlavey
źródło
5

Dla konkretnego przykładu wyobraźmy sobie prosty typ liczb zespolonych:

struct complex {
    double x, y;
    complex(double _x, double _y) : x(_x), y(_y) { }
    complex& operator +=(const complex& b) {
        x += b.x;
        y += b.y;
        return *this;
    }
    complex operator +(const complex& b) {
        complex result(x+b.x, y+b.y);
        return result;
    }
    /* trivial assignment operator */
}

W przypadku a = a + b musi utworzyć dodatkową zmienną tymczasową, a następnie ją skopiować.

Losowo 832
źródło
to jest bardzo ładny przykład, pokazuje jak zaimplementowano 2 operatorów.
Grijesh Chauhan
5

Zadajesz złe pytanie.

Jest mało prawdopodobne, aby wpłynęło to na wydajność aplikacji lub funkcji. Nawet gdyby tak było, sposobem na sprawdzenie jest profilowanie kodu i upewnienie się, jak wpływa on na Ciebie. Zamiast martwić się na tym poziomie, który jest szybszy, o wiele ważniejsze jest myślenie w kategoriach jasności, poprawności i czytelności.

Jest to szczególnie prawdziwe, gdy weźmiesz pod uwagę, że nawet jeśli jest to znaczący czynnik wydajności, kompilatory ewoluują w czasie. Ktoś może wymyślić nową optymalizację, a prawidłowa odpowiedź dzisiaj może okazać się błędna jutro. To klasyczny przypadek przedwczesnej optymalizacji.

Nie oznacza to, że wydajność w ogóle nie ma znaczenia ... Tylko, że jest to niewłaściwe podejście do osiągania celów perfekcyjnych. Właściwym podejściem jest użycie narzędzi do profilowania, aby dowiedzieć się, gdzie Twój kod faktycznie spędza czas, a tym samym na czym należy się skupić.

Joel Coehoorn
źródło
To prawda, ale było to pytanie niskiego poziomu, a nie całościowe pytanie „Kiedy powinienem wziąć pod uwagę taką różnicę”.
Chiffa
1
Pytanie PO było całkowicie uzasadnione, o czym świadczą odpowiedzi innych osób (i głosy za). Chociaż mamy swój punkt (profil pierwsze, etc.) to na pewno jest interesujący wiedzieć tego typu rzeczy - czy naprawdę dzieje się za każdym profilu trywialne stwierdzenie piszesz, profil wynikach każdej decyzji można podjąć? Nawet jeśli na SO są ludzie, którzy już przestudiowali, sprofilowali, zdemontowali sprawę i mają ogólną odpowiedź do udzielenia?
4

Myślę, że powinno to zależeć od maszyny i jej architektury. Jeśli jego architektura umożliwia pośrednie adresowanie pamięci, piszący kompilator MOŻE po prostu użyć tego kodu (do optymalizacji):

mov $[y],$ACC

iadd $ACC, $[i] ; i += y. WHICH MIGHT ALSO STORE IT INTO "i"

Natomiast i = i + ymoże zostać przetłumaczony na (bez optymalizacji):

mov $[i],$ACC

mov $[y],$B 

iadd $ACC,$B

mov $B,[i]


To powiedziawszy, inależy również wziąć pod uwagę inne komplikacje, takie jak if jest funkcją zwracającą wskaźnik itp. Większość kompilatorów na poziomie produkcyjnym, w tym GCC, tworzy ten sam kod dla obu instrukcji (jeśli są to liczby całkowite).

Aniket Inge
źródło
2

Nie, oba sposoby są takie same.

PochmurnoMarmur
źródło
10
Nie, jeśli jest to typ zdefiniowany przez użytkownika z przeciążonymi operatorami.