Czy istnieje różnica w wydajności między, i++
a ++i
jeśli wynikowa wartość nie jest używana?
c
performance
optimization
post-increment
pre-increment
Mark Harrison
źródło
źródło
Odpowiedzi:
Streszczenie: Nie
i++
może potencjalnie być wolniejszy niż++i
, ponieważ stara wartośći
może być konieczne zapisanie do późniejszego użycia, ale w praktyce wszystkie nowoczesne kompilatory zoptymalizują to.Możemy to zademonstrować, patrząc na kod tej funkcji, zarówno za pomocą, jak
++i
ii++
.Pliki są takie same, z wyjątkiem
++i
ii++
:Skompilujemy je, a także uzyskamy wygenerowany asembler:
Widzimy, że zarówno wygenerowany plik obiektu, jak i plik asemblera są takie same.
źródło
++i
zamiasti++
. Nie ma absolutnie żadnego powodu, aby tego nie robić, a jeśli oprogramowanie kiedykolwiek przejdzie przez łańcuch narzędzi, który go nie optymalizuje, oprogramowanie będzie bardziej wydajne. Biorąc pod uwagę, że jest tak samo łatwe do pisania,++i
jak i do pisaniai++
, tak naprawdę nie ma wymówki, aby nie używać++i
w ogóle .Od efektywności do intencji Andrew Koeniga:
I :
Tak więc, jeśli wynikowa wartość nie zostanie użyta, użyłbym
++i
. Ale nie dlatego, że jest bardziej wydajny: ponieważ poprawnie określa moje zamiary.źródło
i++
taki sam sposób, jak jai += n
lubi = i + n
, tj. W postaci docelowego obiektu czasownika , operandem docelowym po lewej stronie operatora czasownika . W przypadku nie ma prawego obiektu , ale reguła nadal obowiązuje, utrzymując cel po lewej stronie operatora czasownika .i++
Lepszą odpowiedzią jest to, że
++i
czasami będzie to szybsze, ale nigdy wolniejsze.Wydaje się, że wszyscy zakładają, że
i
jest to typ wbudowany, taki jakint
. W takim przypadku nie będzie mierzalnej różnicy.Jeśli jednak
i
jest to typ złożony, możesz znaleźć mierzalną różnicę. Dlai++
was musi zrobić kopię swojej klasie przed zwiększając ją. W zależności od tego, co jest zaangażowane w kopię, może rzeczywiście być wolniejsze, ponieważ dzięki niemu++it
możesz po prostu zwrócić ostateczną wartość.Kolejna różnica polega na tym,
++i
że masz opcję zwrotu referencji zamiast wartości. Ponownie, w zależności od tego, co jest zaangażowane w tworzenie kopii obiektu, może to być wolniejsze.Prawdziwym przykładem tego, gdzie może się to zdarzyć, byłoby użycie iteratorów. Kopiowanie iteratora raczej nie będzie wąskim gardłem w twojej aplikacji, ale nadal dobrą praktyką jest nawyk używania
++i
zamiasti++
gdzie nie ma to wpływu na wynik.źródło
Krótka odpowiedź:
Nigdy nie ma żadnej różnicy między
i++
i++i
pod względem szybkości. Dobry kompilator nie powinien generować innego kodu w obu przypadkach.Długa odpowiedź:
W każdej innej odpowiedzi nie ma wzmianki o tym, że różnica między
++i
kontrai++
ma sens tylko w wyrażeniu, które znaleziono.W przypadku
for(i=0; i<n; i++)
,i++
jest sam w swojej wypowiedzi: istnieje punkt sekwencja przedi++
i jeden jest po niej. Zatem jedynym wygenerowanym kodem maszynowym jest „wzrosti
o1
” i dobrze zdefiniowano, w jaki sposób jest on sekwencjonowany w stosunku do reszty programu. Więc jeśli zmienisz go na prefiks++
, nie będzie to miało najmniejszego znaczenia, nadal dostaniesz kod maszynowy „wzrosti
o1
”.Różnice między
++i
ii++
mają znaczenie tylko w wyrażeniach takich jakarray[i++] = x;
versusarray[++i] = x;
. Niektórzy mogą argumentować i twierdzić, że postfiks będzie wolniejszy w takich operacjach, ponieważ rejestr, w którymi
rezyduje, musi zostać przeładowany później. Zwróć jednak uwagę, że kompilator może dowolnie zamawiać instrukcje, o ile nie „zakłóca zachowania abstrakcyjnej maszyny”, jak nazywa ją standard C.Możesz więc założyć, że
array[i++] = x;
zostanie on przetłumaczony na kod maszynowy jako:i
w rejestrze A.i
w rejestrze A // nieefektywną, ponieważ dodatkowe instrukcje tutaj, zrobiliśmy to już raz.i
.kompilator mógłby równie dobrze produkować kod bardziej wydajnie, na przykład:
i
w rejestrze A.i
.Tylko dlatego, że jako programista języka C zostałeś przeszkolony, aby myśleć, że postfiks
++
dzieje się na końcu, kod maszynowy nie musi być zamawiany w ten sposób.Więc nie ma różnicy między prefiksem i postfiksem
++
w C. Teraz, jako programista C, powinieneś się różnić, to ludzie, którzy niekonsekwentnie używają prefiksu w niektórych przypadkach i postfiksa w innych przypadkach, bez żadnego uzasadnienia. Sugeruje to, że nie są pewni, jak działa C lub że mają nieprawidłową znajomość języka. Jest to zawsze zły znak, co z kolei sugeruje, że podejmują inne wątpliwe decyzje w swoim programie, oparte na przesądach lub „dogmatach religijnych”.„Prefiks
++
jest zawsze szybszy” jest rzeczywiście jednym z takich fałszywych dogmatów, które są powszechne wśród niedoszłych programistów C.źródło
Biorąc list od Scotta Meyersa, bardziej skuteczny c ++ pozycja 6: Rozróżnij formy przyrostu i zmniejszenia .
Wersja przedrostka jest zawsze preferowana nad postfiksem w odniesieniu do obiektów, zwłaszcza w odniesieniu do iteratorów.
Powodem tego jest spojrzenie na wzór połączeń operatorów.
Patrząc na ten przykład łatwo zauważyć, że operator prefiksu zawsze będzie bardziej wydajny niż postfiks. Ze względu na potrzebę tymczasowego obiektu w użyciu postfiksa.
Właśnie dlatego, gdy widzisz przykłady wykorzystujące iteratory, zawsze używają wersji prefiksu.
Ale jak wskazujesz na int, nie ma żadnej różnicy ze względu na optymalizację kompilatora, która może mieć miejsce.
źródło
Oto dodatkowa obserwacja, jeśli martwisz się mikrooptymalizacją. Pętle zmniejszające się mogą być bardziej wydajne niż pętle zwiększające (w zależności od architektury zestawu instrukcji, np. ARM), biorąc pod uwagę:
W każdej pętli będziesz mieć jedną instrukcję dla:
1
doi
.i
jest mniejsza niż100
.i
jest mniejsza niż a100
.Podczas gdy pętla zmniejszająca się:
Pętla będzie zawierała instrukcje dla każdego z:
i
, ustawienie flagi statusu rejestru procesora.Z==0
).Oczywiście działa to tylko przy zmniejszeniu do zera!
Zapamiętane z Podręcznika programisty systemu ARM.
źródło
Nie pozwól, aby pytanie „który z nich był szybszy” było decydującym czynnikiem, którego należy użyć. Możliwe, że nigdy nie będziesz się tak przejmować, a poza tym czas czytania programisty jest znacznie droższy niż czas pracy maszyny.
Użyj tego, co jest najbardziej sensowne dla człowieka czytającego kod.
źródło
Po pierwsze: różnica między
i++
i++i
jest nieistotna w C.Do szczegółów.
1. Dobrze znany problem C ++:
++i
jest szybszyW C ++
++i
jest bardziej wydajny iffi
jest rodzajem obiektu z przeciążonym operatorem przyrostowym.Dlaczego?
W
++i
obiekcie obiekt jest najpierw zwiększany, a następnie może być przekazywany jako stałe odwołanie do dowolnej innej funkcji. Nie jest to możliwe, jeśli wyrażenie jestfoo(i++)
spowodowane tym, że teraz należy wykonać przyrost, zanimfoo()
zostanie wywołane, ale stara wartość musi zostać przekazanafoo()
. W związku z tym kompilator jest zmuszony wykonać kopięi
przed wykonaniem operatora przyrostu na oryginale. Dodatkowe wywołania konstruktora / destruktora to zła część.Jak wspomniano powyżej, nie dotyczy to podstawowych typów.
2. Mało znany fakt:
i++
może być szybszyJeśli nie trzeba wywoływać żadnego konstruktora / destruktora, co zawsze ma miejsce w C
++i
ii++
powinno być równie szybkie, prawda? Nie. Są one praktycznie tak samo szybkie, ale mogą występować niewielkie różnice, które większość innych użytkowników odpowiedziała w niewłaściwy sposób.Jak może
i++
być szybciej?Chodzi o zależności danych. Jeśli wartość musi zostać załadowana z pamięci, należy wykonać dwie kolejne operacje, zwiększając ją i wykorzystując. Za
++i
pomocą należy wykonać inkrementację, zanim można będzie użyć wartości. Dziękii++
, użycie nie zależy od przyrostu, a CPU może wykonać operację użycia równolegle z operacją przyrostową. Różnica wynosi co najwyżej jeden cykl CPU, więc jest naprawdę nieistotna, ale tak jest. I jest na odwrót, niż wielu by się spodziewało.źródło
++i
lubi++
jest używane w obrębie innego wyrażenia, zmiana między nimi zmienia semantykę wyrażenia, więc wszelkie możliwe zyski / straty wydajności nie podlegają dyskusji. Jeśli są one autonomiczne, tzn. Wynik operacji nie jest natychmiast używany, to każdy porządny kompilator skompilowałby go do tego samego, na przykładINC
instrukcji asemblera.i++
i++i
mogą być używane zamiennie w prawie każdej możliwej sytuacji poprzez dostosowanie stałych pętli po drugim, więc są one w pobliżu odpowiednika w to, co robią dla programisty. 2) Mimo że oba kompilują się do tej samej instrukcji, ich wykonanie różni się dla procesora. W przypadkui++
, CPU może obliczyć przyrost równolegle do niektórych innych instrukcji, które używają tej samej wartości (procesory naprawdę to robią!), Podczas gdy z++i
CPU musi zaplanować inną instrukcję po zwiększeniu.if(++foo == 7) bar();
iif(foo++ == 6) bar();
są funkcjonalnie równoważne. Jednak drugi może być o jeden cykl szybszy, ponieważ proces porównywania i przyrostu może być obliczany równolegle. Nie chodzi o to, że ten pojedynczy cykl ma duże znaczenie, ale jest różnica.<
na przykład vs<=
) tam, gdzie++
są zwykle używane, więc konwersja między nimi jest często łatwo możliwa.@Mark Chociaż kompilator może zoptymalizować tymczasową kopię zmiennej (na podstawie stosu), a gcc (w najnowszych wersjach) robi to, nie oznacza to, że wszystkie kompilatory zawsze to zrobią.
Właśnie przetestowałem go z kompilatorami, których używamy w naszym obecnym projekcie i 3 na 4 go nie optymalizują.
Nigdy nie zakładaj, że kompilator działa poprawnie, szczególnie jeśli możliwie szybszy, ale nigdy wolniejszy kod jest tak łatwy do odczytania.
Jeśli nie masz naprawdę głupiej implementacji jednego z operatorów w kodzie:
Alwas wolę ++ i niż i ++.
źródło
W C kompilator może ogólnie zoptymalizować je tak, aby były takie same, jeśli wynik nie jest używany.
Jednak w C ++, jeśli używane są inne typy, które zapewniają własne operatory ++, wersja prefiksu prawdopodobnie będzie szybsza niż wersja postfiksowa. Tak więc, jeśli nie potrzebujesz semantyki Postfiksa, lepiej użyć operatora prefiksu.
źródło
Mogę wymyślić sytuację, w której postfiks jest wolniejszy niż przyrost prefiksu:
Wyobraź sobie, że procesor z rejestrem
A
jest używany jako akumulator i jest to jedyny rejestr używany w wielu instrukcjach (niektóre małe mikrokontrolery są w rzeczywistości takie).Teraz wyobraź sobie następujący program i ich tłumaczenie na hipotetyczny zestaw:
Przyrost prefiksu:
Przyrost Postfix:
Zwróć uwagę, w jaki sposób wartość
b
została zmuszona do ponownego załadowania. Z przyrostem prefiksu kompilator może po prostu zwiększyć wartość i kontynuować korzystanie z niej, być może unikając ponownego załadowania, ponieważ żądana wartość znajduje się już w rejestrze po zwiększeniu. Jednak z przyrostem postfiksowym kompilator musi poradzić sobie z dwiema wartościami, jedną starą, a drugą przyrostową, która, jak pokazano powyżej, powoduje jeszcze jeden dostęp do pamięci.Oczywiście, jeśli wartość przyrostu nie jest używana, na przykład pojedyncza
i++;
instrukcja, kompilator może (i robi) po prostu wygenerować instrukcję przyrostu bez względu na użycie postfiksu lub prefiksu.Na marginesie chciałbym wspomnieć, że wyrażenie, w którym istnieje wyrażenie, nie
b++
może zostać po prostu przekonwertowane na jedno++b
bez żadnego dodatkowego wysiłku (na przykład poprzez dodanie a- 1
). Zatem porównanie tych dwóch, jeśli są częścią jakiegoś wyrażenia, nie jest tak naprawdę poprawne. Często, gdy używaszb++
wyrażenia, którego nie możesz użyć++b
, więc nawet gdyby++b
były potencjalnie bardziej wydajne, byłoby to po prostu błędne. Wyjątek stanowi oczywiście prośba o wyrażenie (na przykład,a = b++ + 1;
które można zmienić naa = ++b;
).źródło
Czytałem przez większość odpowiedzi tutaj i wielu komentarzach, a ja nie widziałem żadnego odniesienia do jednej instancji, że mogę myśleć, gdzie
i++
jest bardziej wydajny niż++i
(i być może zaskakująco--i
był bardziej skuteczny niżi--
). To jest dla kompilatorów C dla DEC PDP-11!PDP-11 miał instrukcje montażu dotyczące wstępnego zmniejszania rejestru i przyrostowego, ale nie na odwrót. Instrukcje pozwoliły na użycie dowolnego rejestru „ogólnego przeznaczenia” jako wskaźnika stosu. Więc jeśli użyłeś czegoś takiego
*(i++)
, można go skompilować w pojedynczą instrukcję montażu, a*(++i)
nie mógł.Jest to oczywiście bardzo ezoteryczny przykład, ale zapewnia on wyjątek, w którym post-inkrementacja jest bardziej wydajna (lub powinienem powiedzieć , że tak było , ponieważ w dzisiejszych czasach nie ma dużego zapotrzebowania na kod C w PDP-11).
źródło
--i
ii++
.Zawsze wolę wstępne zwiększenie, jednak ...
Chciałem zauważyć, że nawet w przypadku wywołania funkcji operator ++, kompilator będzie w stanie zoptymalizować tymczasowe, jeśli funkcja zostanie wstawiona. Ponieważ operator ++ jest zwykle krótki i często implementowany w nagłówku, prawdopodobnie zostanie wstawiony.
Tak więc, dla celów praktycznych, prawdopodobnie nie ma dużej różnicy między wydajnością tych dwóch form. Zawsze jednak wolę wstępne zwiększenie, ponieważ wydaje się, że lepiej jest bezpośrednio wyrazić to, co próbuję powiedzieć, niż polegać na optymalizatorze, aby to ustalić.
Ponadto, mniej prawdopodobne, że optmizer będzie mniej wymagał, oznacza, że kompilator działa szybciej.
źródło
Moje C jest trochę zardzewiałe, więc z góry przepraszam. Speedwise rozumiem wyniki. Ale jestem zdezorientowany co do tego, jak oba pliki wyszły do tego samego skrótu MD5. Może pętla for działa tak samo, ale czy poniższe 2 wiersze kodu nie wygenerowałyby innego zestawu?
vs
Pierwszy zapisuje wartość do tablicy, a następnie zwiększa i. Drugie przyrosty zapisuję następnie w tablicy. Nie jestem ekspertem od montażu, ale po prostu nie widzę, jak te same pliki wykonywalne byłyby generowane przez te 2 różne wiersze kodu.
Tylko moje dwa centy.
źródło
foo[i++]
sięfoo[++i]
bez zmieniania czegokolwiek innego będzie oczywiście zmienić semantykę program, ale w przypadku niektórych procesorów przy użyciu kompilatora bez podnoszenia pętli logiki optymalizacji, zwiększającp
iq
raz, a następnie uruchomić pętlę, która wykonuje np*(p++)=*(q++);
byłoby szybsze niż za pomocą pętli, która wykonuje*(++pp)=*(++q);
. W przypadku bardzo ciasnych pętli na niektórych procesorach różnica prędkości może być znacząca (ponad 10%), ale jest to prawdopodobnie jedyny przypadek w C, w którym przyrostowo jest znacznie szybszy niż przyrost wstępny.