Unikaj operatora przyrostowego Postfix

25

Przeczytałem, że powinienem unikać operatora inkrementacji postfiksów ze względu na wydajność (w niektórych przypadkach).

Ale czy to nie wpływa na czytelność kodu? W mojej opinii:

for(int i = 0; i < 42; i++);
    /* i will never equal 42! */

Wygląda lepiej niż:

for(int i = 0; i < 42; ++i);
    /* i will never equal 42! */

Ale to prawdopodobnie po prostu z przyzwyczajenia. Trzeba przyznać, że nie widziałem wielu zastosowań ++i.

Czy w tym przypadku wydajność jest tak niekorzystna, że ​​obniża czytelność? Czy jestem po prostu ślepy i czy ++ijest bardziej czytelny niż i++?

Mateen Ulhaq
źródło
1
Użyłem, i++zanim wiedziałem, że może to wpłynąć na wydajność ++i, więc przełączyłem się. Na początku ten ostatni wyglądał trochę dziwnie, ale po pewnym czasie przyzwyczaiłem się do tego, a teraz wydaje się tak naturalny jak i++.
gablin
15
++ii i++rób różne rzeczy w pewnych kontekstach, nie zakładaj, że są takie same.
Orbling
2
Czy chodzi o C czy C ++? Są to dwa bardzo różne języki! :-) W C ++ idiomatyczna pętla for jest for (type i = 0; i != 42; ++i). Nie tylko może operator++być przeciążony, ale może operator!=i operator<. Przyrost prefiksu nie jest droższy niż postfiks, nierówny nie jest droższy niż mniejszy niż. Z których powinniśmy korzystać?
Bo Persson
7
Czy nie powinien się nazywać ++ C?
Armand
21
@Stephen: C ++ oznacza weź C, dodaj do niego, a następnie użyj starego .
supercat

Odpowiedzi:

58

Fakty:

  1. i ++ i ++ i są równie łatwe do odczytania. Nie lubisz jednego, ponieważ nie jesteś do tego przyzwyczajony, ale zasadniczo nie ma niczego, co można by błędnie zinterpretować, więc nie trzeba już czytać ani pisać.

  2. W przynajmniej niektórych przypadkach operator postfiksów będzie mniej wydajny.

  3. Jednak w 99,99% przypadków nie będzie to miało znaczenia, ponieważ (a) i tak będzie działał na prostym lub prymitywnym typie i jest to tylko problem, jeśli kopiuje duży obiekt (b), nie będzie występował krytyczna część kodu (c) nie wiesz, czy kompilator go zoptymalizuje, czy nie, może to zrobić.

  4. Dlatego sugeruję używanie prefiksu, chyba że konkretnie postfiks jest dobrym nawykiem, aby się do niego przyzwyczaić, tylko dlatego, że (a) dobrym nawykiem jest precyzowanie innych rzeczy i (b) raz na niebieskim księżycu będziesz chciał użyć postfiksu i odwrotnie: jeśli zawsze piszesz, co masz na myśli, jest to mniej prawdopodobne. Zawsze występuje kompromis między wydajnością a optymalizacją.

Powinieneś stosować zdrowy rozsądek, a nie mikrooptymalizować, dopóki nie będziesz tego potrzebować, ale nie bądź rażąco nieefektywny ze względu na to. Zazwyczaj oznacza to: po pierwsze, wyklucz konstrukcję kodu, która jest niedopuszczalnie nieefektywna nawet w kodzie niekrytycznym czasowo (zwykle coś reprezentuje podstawowy błąd koncepcyjny, taki jak przekazywanie wartości 500 MB obiektów bez powodu); a po drugie, z każdego innego sposobu pisania kodu wybierz najczystszy.

Jednak tutaj uważam, że odpowiedź jest prosta: uważam, że pisanie prefiksu, chyba że potrzebujesz konkretnie postfiksa, jest (a) bardzo marginalnie wyraźniejsze i (b) bardzo marginalnie bardziej wydajne, więc zawsze powinieneś pisać to domyślnie, ale nie martw się, jeśli zapomnisz.

Sześć miesięcy temu myślałem tak samo jak ty, że i ++ jest bardziej naturalny, ale jest to po prostu to, do czego przywykłeś.

EDYCJA 1: Scott Meyers w „Bardziej efektywnym C ++”, któremu ogólnie ufam w tej kwestii, mówi, że ogólnie powinieneś unikać używania operatora Postfiksa na typach zdefiniowanych przez użytkownika (ponieważ jedyną rozsądną implementacją funkcji przyrostu Postfiksa jest utworzenie kopiowanie obiektu, wywołaj funkcję inkrementacji prefiksu, aby wykonać inkrementację i zwróć kopię, ale operacje kopiowania mogą być kosztowne).

Nie wiemy więc, czy istnieją jakieś ogólne zasady dotyczące (a) czy jest to prawdą dzisiaj, (b) czy ma to zastosowanie (w mniejszym stopniu) do typów wewnętrznych (c) czy powinieneś używać „++” na cokolwiek więcej niż lekka klasa iteratorów. Ale ze wszystkich powodów, które opisałem powyżej, nie ma znaczenia, rób to, co powiedziałem wcześniej.

EDYCJA 2: Odnosi się do ogólnej praktyki. Jeśli uważasz, że to ma znaczenie w konkretnym przypadku, powinieneś go profilować i zobaczyć. Profilowanie jest łatwe i tanie i działa. Odejście od pierwszych zasad, które należy zoptymalizować, jest trudne i kosztowne i nie działa.

Jack V.
źródło
Twój post dotyczy pieniędzy. W wyrażeniach, w których operator infix + i post-increment ++ zostały przeciążone, takich jak aClassInst = someOtherClassInst + yetAnotherClassInst ++, analizator składni wygeneruje kod w celu wykonania operacji addytywnej przed wygenerowaniem kodu w celu wykonania operacji po zwiększeniu, co zmniejszy potrzebę utwórz tymczasową kopię. Zabójca wydajności tutaj nie ma post-przyrostu. Jest to użycie przeciążonego operatora infix. Operatorzy Infix produkują nowe wystąpienia.
bit-twiddler
2
Podejrzewam, że powodem, dla którego ludzie są „przyzwyczajeni”, i++a nie z ++ipowodu nazwy pewnego popularnego języka programowania, o którym mowa w tym pytaniu / odpowiedzi ...
Shadow
61

Zawsze najpierw koduj programistę, a drugi komputer.

Jeśli występuje różnica w wydajności, po tym, jak kompilator rzuci okiem na Twój kod, ORAZ możesz go zmierzyć ORAZ ma to znaczenie - możesz go zmienić.

Martin Beckett
źródło
7
Oświadczenie SUPERB !!!
Dave
8
@ Martin: właśnie dlatego użyłbym przyrostu prefiksu. Semantyka Postfiksa oznacza utrzymanie starej wartości w pobliżu, a jeśli nie ma takiej potrzeby, użycie jej jest niedokładne.
Matthieu M.
1
Dla indeksu pętli, który byłby wyraźniejszy - ale jeśli iterowałeś po tablicy przez zwiększenie wskaźnika i użycie prefiksu, oznaczało to rozpoczęcie od nielegalnego adresu jeden przed startem, co byłoby złe niezależnie od zwiększenia wydajności
Martin Beckett
5
@Matthew: Po prostu nie jest prawdą, że post-inkrement oznacza utrzymywanie kopii starej wartości w pobliżu. Nie można być pewnym, jak kompilator obsługuje wartości pośrednie, dopóki nie obejrzy się jego danych wyjściowych. Jeśli poświęcisz trochę czasu, aby wyświetlić moją listę assemblowanych języków asemblera wygenerowanych przez GCC, zobaczysz, że GCC generuje ten sam kod maszynowy dla obu pętli. Ten nonsens o faworyzowaniu wstępnego przyrostu nad późniejszym, ponieważ jest bardziej wydajny, to niewiele więcej niż przypuszczenie.
bit-twiddler
2
@Mathhieu: Kod, który opublikowałem został wygenerowany z wyłączoną optymalizacją. Specyfikacja C ++ nie określa, że ​​kompilator musi wygenerować tymczasową instancję wartości, gdy używana jest przyrostowa pozycja. Określa jedynie pierwszeństwo operatorów przed i po zwiększeniu.
bit-twiddler
13

GCC tworzy ten sam kod maszynowy dla obu pętli.

Kod C.

int main(int argc, char** argv)
{
    for (int i = 0; i < 42; i++)
            printf("i = %d\n",i);

    for (int i = 0; i < 42; ++i)
        printf("i = %d\n",i);

    return 0;
}

Kod zgromadzenia (z moimi komentarzami)

    cstring
LC0:
    .ascii "i = %d\12\0"
    .text
.globl _main
_main:
    pushl   %ebp
    movl    %esp, %ebp
    pushl   %ebx
    subl    $36, %esp
    call    L9
"L00000000001$pb":
L9:
    popl    %ebx
    movl    $0, -16(%ebp)  // -16(%ebp) is "i" for the first loop 
    jmp L2
L3:
    movl    -16(%ebp), %eax   // move i for the first loop to the eax register 
    movl    %eax, 4(%esp)     // push i onto the stack
    leal    LC0-"L00000000001$pb"(%ebx), %eax // load the effective address of the format string into the eax register
    movl    %eax, (%esp)      // push the address of the format string onto the stack
    call    L_printf$stub    // call printf
    leal    -16(%ebp), %eax  // make the eax register point to i
    incl    (%eax)           // increment i
L2:
    cmpl    $41, -16(%ebp)  // compare i to the number 41
    jle L3              // jump to L3 if less than or equal to 41
    movl    $0, -12(%ebp)   // -12(%ebp) is "i" for the second loop  
    jmp L5
L6:
    movl    -12(%ebp), %eax   // move i for the second loop to the eax register 
    movl    %eax, 4(%esp)     // push i onto the stack
    leal    LC0-"L00000000001$pb"(%ebx), %eax // load the effective address of the format string into the eax register
    movl    %eax, (%esp)      // push the address of the format string onto the stack
    call    L_printf$stub     // call printf
    leal    -12(%ebp), %eax  // make eax point to i
    incl    (%eax)           // increment i
L5:
    cmpl    $41, -12(%ebp)   // compare i to 41 
    jle L6               // jump to L6 if less than or equal to 41
    movl    $0, %eax
    addl    $36, %esp
    popl    %ebx
    leave
    ret
    .section __IMPORT,__jump_table,symbol_stubs,self_modifying_code+pure_instructions,5
L_printf$stub:
    .indirect_symbol _printf
    hlt ; hlt ; hlt ; hlt ; hlt
    .subsections_via_symbols
bit-twiddler
źródło
A może przy włączonej optymalizacji?
serv-inc
2
@ użytkownik: Prawdopodobnie nie ma zmian, ale czy naprawdę spodziewasz się, że wkrótce zmieni się bit-twiddler?
Deduplicator
2
Uważaj: podczas gdy w C nie ma typów zdefiniowanych przez użytkownika z przeciążonymi operatorami, w C ++ istnieją, a generalizacja od typów podstawowych do typów zdefiniowanych przez użytkownika jest po prostu nieprawidłowa .
Deduplicator
@Deduplicator: Dziękujemy również za zwrócenie uwagi na to, że ta odpowiedź nie uogólnia się na typy zdefiniowane przez użytkownika. Nie pytałem o jego stronę użytkownika.
serv-inc
12

Nie przejmuj się wydajnością, powiedzmy 97% czasu. Przedwczesna optymalizacja jest źródłem wszelkiego zła.

- Donald Knuth

Teraz, gdy to nam przeszkadza, dokonajmy właściwego wyboru :

  • ++i: przyrost prefiksu , przyrost bieżącej wartości i wynik
  • i++: przyrost postfiksa , skopiuj wartość, zwiększ aktualną wartość, otrzymasz kopię

O ile nie jest wymagana kopia starej wartości, zastosowanie przyrostu postfiksowego jest okrągłym sposobem na załatwienie sprawy.

Niedokładność wynika z lenistwa, zawsze używaj konstrukcji, która wyraża twoje zamiary w najbardziej bezpośredni sposób, istnieje mniejsze prawdopodobieństwo, że przyszły opiekun może źle zrozumieć twoje pierwotne zamiary.

Mimo, że jest (naprawdę) niewielki, zdarza się, że czytam kod. Zastanawiam się, czy intencja i faktyczna ekspresja się pokrywają, i oczywiście po kilku miesiącach oni (lub ja) też nie pamiętałem ...

Tak więc nie ma znaczenia, czy wygląda to dobrze dla Ciebie, czy nie. Embrace KISS . Za kilka miesięcy porzucisz swoje stare praktyki.

Matthieu M.
źródło
4

W C ++ można znacznie poprawić wydajność, jeśli występują przeciążenia operatora, szczególnie jeśli piszesz kodowany szablon i nie wiesz, co może być przekazane. I logika stojąca za każdym iteratorem X może być zarówno znaczna, jak i znacząca - to jest powolny i niemożliwy do zoptymalizowania przez kompilator.

Ale nie jest tak w C, gdzie wiesz, że będzie to tylko trywialny typ, a różnica w wydajności jest trywialna, a kompilator może łatwo zoptymalizować.

Wskazówka: programujesz w języku C lub C ++, a pytania dotyczą jednego lub drugiego, a nie obu.

DeadMG
źródło
2

Wydajność każdej operacji jest wysoce zależna od podstawowej architektury. Należy zwiększyć wartość przechowywaną w pamięci, co oznacza, że ​​wąskie gardło von Neumanna jest czynnikiem ograniczającym w obu przypadkach.

W przypadku ++ i musimy

Fetch i from memory 
Increment i
Store i back to memory
Use i

W przypadku i ++ musimy

Fetch i from memory
Use i
Increment i
Store i back to memory

Operatory ++ i - śledzą swoje pochodzenie w zestawie instrukcji PDP-11. PDP-11 może wykonać automatyczny post-inkrement w rejestrze. Może również wykonywać automatyczne wstępne zmniejszenie wartości efektywnego adresu zawartego w rejestrze. W obu przypadkach kompilator może skorzystać z tych operacji na poziomie komputera tylko wtedy, gdy dana zmienna jest zmienną „rejestrową”.

bit-twiddler
źródło
2

Jeśli chcesz wiedzieć, czy coś jest wolne, sprawdź to. Weź BigInteger lub ekwiwalent, włóż go w podobną pętlę for, używając obu idiomów, upewnij się, że wnętrze pętli nie zostanie zoptymalizowane, i zmierz czas ich obu.

Po przeczytaniu artykułu nie przekonuje mnie to z trzech powodów. Po pierwsze, kompilator powinien być w stanie zoptymalizować tworzenie obiektu, który nigdy nie jest używany. Po drugie, i++koncepcja jest idiomatyczna dla liczb dla pętli , więc przypadki, w których widzę, że są dotknięte, są ograniczone. Po trzecie, dostarczają argument czysto teoretyczny, bez liczb na poparcie.

Biorąc pod uwagę powód nr 1, domyślam się, że kiedy faktycznie mierzysz czas, będą one obok siebie.

jprete
źródło
-1

Przede wszystkim nie wpływa na czytelność IMO. To nie jest to, co zwykle widziałeś, ale minie trochę czasu, zanim się przyzwyczaisz.

Po drugie, chyba że użyjesz wielu operatorów Postfiksów w kodzie, prawdopodobnie nie zobaczysz dużej różnicy. Głównym argumentem za nieużywaniem ich, gdy jest to możliwe, jest to, że kopia wartości oryginalnego var musi być przechowywana do końca argumentów, w których oryginalny var mógł być nadal używany. To 32 bity lub 64 bity w zależności od architektury. Odpowiada to 4 lub 8 bajtom lub 0,00390625 lub 0,0078125 MB. Szanse są bardzo duże, że jeśli nie użyjesz ich mnóstwa, które trzeba oszczędzać przez bardzo długi czas, że przy dzisiejszych zasobach komputerowych i szybkości nie zauważysz nawet różnicy, zmieniając postfiks na prefiks.

EDYCJA: Zapomnij o tej pozostałej części, ponieważ mój wniosek okazał się fałszywy (z wyjątkiem części ++ i i ++, która nie zawsze robi to samo ... to nadal prawda).

Wcześniej zauważono również, że nie robią tego samego w sprawach. Uważaj na dokonanie zmiany, jeśli zdecydujesz. Nigdy tego nie wypróbowałem (zawsze używałem postfiksu), więc nie wiem na pewno, ale myślę, że przejście z postfiksu na prefiks przyniesie różne wyniki: (znowu mogę się mylić ... zależy od kompilatora / tłumacz też)

for (int i=0; i < 10; i++) //the set of i values here will be {0,1,2,3,4,5,6,7,8,9}
for (int i=0; i < 10; ++i) //the set of i values here will be {1,2,3,4,5,6,7,8,9,10}
Kenneth
źródło
4
Operacja przyrostowa występuje na końcu pętli for, więc miałyby dokładnie taką samą moc wyjściową. Nie zależy to od kompilatora / interpretera.
jsternberg
@ jsternberg ... Dzięki Nie byłem pewien, kiedy nastąpił przyrost, ponieważ tak naprawdę nigdy nie miałem powodu, aby go przetestować. Minęło dużo czasu, odkąd robiłem kompilatory na studiach! lol
Kenneth
Źle, źle, źle.
ruohola
-1

Myślę, że semantycznie ++ima więcej sensu i++, więc trzymałbym się pierwszego, z wyjątkiem tego, że często tego nie robię (tak jak w Javie, gdzie powinieneś używać, i++ponieważ jest powszechnie używany).

Oliver Weiler
źródło
-2

Nie chodzi tylko o wydajność.

Czasami chcesz w ogóle uniknąć kopiowania, ponieważ nie ma to sensu. A ponieważ użycie przyrostu prefiksu nie zależy od tego, po prostu łatwiej jest trzymać się formy prefiksu.

I używanie różnych przyrostów dla typów pierwotnych i typów złożonych ... to naprawdę nieczytelne.

maxim1000
źródło
-2

Chyba że naprawdę go potrzebujesz, trzymałbym się ++. W większości przypadków tak właśnie się zamierza. Niezbyt często potrzebujesz i ++ i zawsze musisz przemyśleć dwa razy, czytając taki konstrukt. Z ++ i jest to łatwe: dodajesz 1, używasz, a następnie i pozostaje taki sam.

Tak więc, zgadzam się z serdecznością z @martin beckett: ułatw sobie, to już jest wystarczająco trudne.

Peter Frings
źródło