Czy istnieje różnica w wydajności między, i++
a ++i
jeśli wynikowa wartość nie jest używana?
Czy istnieje różnica w wydajności między, i++
a ++i
jeśli wynikowa wartość nie jest używana?
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
i i++
.
$ cat i++.c
extern void g(int i);
void f()
{
int i;
for (i = 0; i < 100; i++)
g(i);
}
Pliki są takie same, z wyjątkiem ++i
i i++
:
$ diff i++.c ++i.c
6c6
< for (i = 0; i < 100; i++)
---
> for (i = 0; i < 100; ++i)
Skompilujemy je, a także uzyskamy wygenerowany asembler:
$ gcc -c i++.c ++i.c
$ gcc -S i++.c ++i.c
Widzimy, że zarówno wygenerowany plik obiektu, jak i plik asemblera są takie same.
$ md5 i++.s ++i.s
MD5 (i++.s) = 90f620dda862cd0205cd5db1f2c8c06e
MD5 (++i.s) = 90f620dda862cd0205cd5db1f2c8c06e
$ md5 *.o
MD5 (++i.o) = dd3ef1408d3a9e4287facccec53f7d22
MD5 (i++.o) = dd3ef1408d3a9e4287facccec53f7d22
++i
zamiast i++
. 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 pisania i++
, tak naprawdę nie ma wymówki, aby nie używać ++i
w ogóle .
Od efektywności do intencji Andrew Koeniga:
Po pierwsze, nie jest oczywiste, że
++i
jest bardziej wydajny niżi++
, przynajmniej w przypadku zmiennych całkowitych.
I :
Pytanie, które należy zadać, to nie to, która z tych dwóch operacji jest szybsza, ale która z tych dwóch operacji dokładniej wyraża to, co próbujesz osiągnąć. Twierdzę, że jeśli nie używasz wartości wyrażenia, nigdy nie ma powodu, aby używać
i++
zamiast tego++i
, ponieważ nigdy nie ma powodu, aby kopiować wartość zmiennej, zwiększać zmienną, a następnie wyrzucić kopię.
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.
i++
taki sam sposób, jak ja i += n
lub i = 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 jak int
. 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ę. Dla i++
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ść.
Foo Foo::operator++()
{
Foo oldFoo = *this; // copy existing value - could be slow
// yadda yadda, do increment
return oldFoo;
}
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.
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 przed i++
i jeden jest po niej. Zatem jedynym wygenerowanym kodem maszynowym jest „wzrost i
o 1
” 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 „wzrost i
o 1
”.
Różnice między ++i
i i++
mają znaczenie tylko w wyrażeniach takich jak array[i++] = x;
versus array[++i] = x;
. Niektórzy mogą argumentować i twierdzić, że postfiks będzie wolniejszy w takich operacjach, ponieważ rejestr, w którym i
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.
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.
// Prefix
Integer& Integer::operator++()
{
*this += 1;
return *this;
}
// Postfix
const Integer Integer::operator++(int)
{
Integer oldValue = *this;
++(*this);
return oldValue;
}
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.
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ę:
for (i = 0; i < 100; i++)
W każdej pętli będziesz mieć jedną instrukcję dla:
1
do i
. i
jest mniejsza niż 100
.i
jest mniejsza niż a 100
.Podczas gdy pętla zmniejszająca się:
for (i = 100; i != 0; i--)
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.
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.
Po pierwsze: różnica między i++
i ++i
jest nieistotna w C.
Do szczegółów.
++i
jest szybszyW C ++ ++i
jest bardziej wydajny iff i
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 jest foo(i++)
spowodowane tym, że teraz należy wykonać przyrost, zanim foo()
zostanie wywołane, ale stara wartość musi zostać przekazana foo()
. 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.
i++
może być szybszyJeśli nie trzeba wywoływać żadnego konstruktora / destruktora, co zawsze ma miejsce w C ++i
i i++
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ęki i++
, 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.
++i
lub i++
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ład INC
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 przypadku i++
, 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();
i if(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 ++.
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.
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:
a = ++b + c;
; increment b
LD A, [&b]
INC A
ST A, [&b]
; add with c
ADD A, [&c]
; store in a
ST A, [&a]
Przyrost Postfix:
a = b++ + c;
; load b
LD A, [&b]
; add with c
ADD A, [&c]
; store in a
ST A, [&a]
; increment b
LD A, [&b]
INC A
ST A, [&b]
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żywasz b++
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ć na a = ++b;
).
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).
--i
i i++
.
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.
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?
myArray[i++] = "hello";
vs
myArray[++i] = "hello";
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.
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ąc p
i q
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.