Mamy pytanie, czy istnieje różnica w wydajności między Ci++
i ++i
C ?
Jaka jest odpowiedź na C ++?
c++
performance
oop
post-increment
pre-increment
Mark Harrison
źródło
źródło
Odpowiedzi:
[Streszczenie: Użyj,
++i
jeśli nie masz konkretnego powodu do użyciai++
.]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ówczasi++
i++i
wywołuje jedną zoperator++
funkcji. Oto standardowa para tych funkcji: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ść.źródło
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:
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.
źródło
C t(*this); ++(*this); return t;
W drugim wierszu zwiększasz prawidłowo ten wskaźnik, więc jakt
się go aktualizuje, jeśli to zwiększasz. Czy wartości tego nie zostały już skopiowanet
?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
Przyrost O (n)
Test
Wyniki
Wyniki (czasy w sekundach) z g ++ 4.5 na maszynie wirtualnej:
Przyrost O (1)
Test
Weźmy teraz następujący plik:
Przyrostu nie robi nic. To symuluje przypadek, gdy przyrost ma stałą złożoność.
Wyniki
Wyniki są teraz bardzo różne:
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ówiincrement i, I am interested in the previous value, though
.++i
mówiincrement i, I am interested in the current value
lubincrement 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.
źródło
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, npparent 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:
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:
Wynikało z tego:
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.
źródło
W Google C ++ styl przewodnik mówi:
źródło
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
@Ketan
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żywaniei++
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ą.źródło
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ć.
źródło
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ódło
Różnica w wydajności pomiędzy
++i
ii++
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żywaneint
tak, jakby to byłostruct
.++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: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ść: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
lubclass
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.źródło
@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)
źródło
@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
źródło
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.
źródło
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:
Utwórz następujący zespół:
Widzisz, że dla ++ i b ++ jest to także mnemonik, więc jest to ta sama operacja;)
źródło
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.
źródło
++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
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.
źródło
++i
jest szybszy niżi++
dlatego, że nie zwraca starej kopii wartości.Jest również bardziej intuicyjny:
Ten przykład C wypisuje „02” zamiast „12”, którego można się spodziewać:
To samo dla C ++ :
źródło