Czy <szybciej niż <=?


1574

Jest if( a < 901 )szybszy niż if( a <= 900 ).

Niezupełnie jak w tym prostym przykładzie, ale występują niewielkie zmiany wydajności w złożonym kodzie pętli. Podejrzewam, że ma to coś wspólnego z wygenerowanym kodem maszynowym, na wypadek, gdyby było to w ogóle prawdą.


153
Nie widzę powodu, dla którego to pytanie powinno zostać zamknięte (a zwłaszcza nie usunięte, jak pokazują obecnie głosy), biorąc pod uwagę jego historyczne znaczenie, jakość odpowiedzi i fakt, że pozostałe najważniejsze pytania dotyczące wyników pozostają otwarte. Co najwyżej powinien być zamknięty. Ponadto, nawet jeśli samo pytanie jest źle poinformowane / naiwne, fakt, że pojawiło się ono w książce, oznacza, że ​​pierwotne błędne informacje istnieją gdzieś w „wiarygodnych” źródłach, a zatem pytanie to jest konstruktywne, ponieważ pomaga to wyjaśnić.
Jason C

32
Nigdy nie powiedziałeś nam, do której książki się odnosisz.
Jonathon Reinhart

159
Pisanie <jest dwa razy szybsze niż pisanie <=.
Deqing

6
Tak było w 8086 r.
Jozuego

7
Liczba pozytywnych opinii wyraźnie pokazuje, że istnieją setki osób, które mocno nadmiernie optymalizują.
m93a

Odpowiedzi:


1703

Nie, na większości architektur nie będzie szybciej. Nie określiłeś, ale na x86 wszystkie integralne porównania będą zazwyczaj zaimplementowane w dwóch instrukcjach maszyny:

  • A testlub cmpinstrukcja, która ustawiaEFLAGS
  • I Jccinstrukcja (skok) , w zależności od typu porównania (i układu kodu):
    • jne - Skok, jeśli nie jest równy -> ZF = 0
    • jz - Skok, jeśli zero (równe) -> ZF = 1
    • jg - Skok, jeśli większy -> ZF = 0 and SF = OF
    • (itp...)

Przykład (edytowany dla zwięzłości) Skompilowany z$ gcc -m32 -S -masm=intel test.c

    if (a < b) {
        // Do something 1
    }

Kompiluje do:

    mov     eax, DWORD PTR [esp+24]      ; a
    cmp     eax, DWORD PTR [esp+28]      ; b
    jge     .L2                          ; jump if a is >= b
    ; Do something 1
.L2:

I

    if (a <= b) {
        // Do something 2
    }

Kompiluje do:

    mov     eax, DWORD PTR [esp+24]      ; a
    cmp     eax, DWORD PTR [esp+28]      ; b
    jg      .L5                          ; jump if a is > b
    ; Do something 2
.L5:

Tak więc jedyną różnicą między nimi jest instrukcja jgkontra jgeinstrukcja. Obie zajmie tyle samo czasu.


Chciałbym odnieść się do komentarza, że ​​nic nie wskazuje na to, że różne instrukcje skoków zajmują tyle samo czasu. Odpowiedź na to pytanie jest nieco trudna, ale oto, co mogę dać: w Zestawie instrukcji Intel wszystkie są zgrupowane w ramach jednej wspólnej instrukcji Jcc(Przeskocz, jeśli warunek jest spełniony). Ta sama grupa została utworzona razem w Podręczniku optymalizacji , w Dodatku C. Opóźnienie i przepustowość.

Opóźnienie - liczba cykli zegara wymaganych przez rdzeń wykonawczy do wykonania wszystkich mikroprocesorów tworzących instrukcję.

Przepustowość - liczba cykli zegara wymaganych do oczekiwania, zanim porty wydające będą mogły ponownie zaakceptować tę samą instrukcję. W przypadku wielu instrukcji przepustowość instrukcji może być znacznie mniejsza niż jej opóźnienie

Wartości dla Jcc:

      Latency   Throughput
Jcc     N/A        0.5

z następującym przypisem Jcc:

7) Wybór instrukcji skoku warunkowego powinien opierać się na zaleceniu z sekcji 3.4.1, „Optymalizacja przewidywania rozgałęzień”, aby poprawić przewidywalność rozgałęzień. Po pomyślnym przewidywaniu rozgałęzień opóźnienie jccwynosi zero.

Zatem nic w dokumentach Intela nigdy nie traktuje jednej Jccinstrukcji inaczej niż inne.

Jeśli zastanowimy się nad rzeczywistym zespołem obwodów zastosowanym do wykonania instrukcji, można założyć, że na różnych bitach będą proste bramki ORAZ / OR EFLAGS, aby ustalić, czy warunki są spełnione. Nie ma zatem powodu, aby instrukcja testująca dwa bity zajmowała więcej lub mniej czasu niż jedna testująca tylko jeden (ignorowanie opóźnienia propagacji bramki, które jest znacznie krótsze niż okres zegara).


Edycja: zmiennoprzecinkowy

Odnosi się to również do zmiennoprzecinkowego x87: (Prawie taki sam kod jak powyżej, ale z doublezamiast int.)

        fld     QWORD PTR [esp+32]
        fld     QWORD PTR [esp+40]
        fucomip st, st(1)              ; Compare ST(0) and ST(1), and set CF, PF, ZF in EFLAGS
        fstp    st(0)
        seta    al                     ; Set al if above (CF=0 and ZF=0).
        test    al, al
        je      .L2
        ; Do something 1
.L2:

        fld     QWORD PTR [esp+32]
        fld     QWORD PTR [esp+40]
        fucomip st, st(1)              ; (same thing as above)
        fstp    st(0)
        setae   al                     ; Set al if above or equal (CF=0).
        test    al, al
        je      .L5
        ; Do something 2
.L5:
        leave
        ret

239
@Dyppl faktycznie jgi jnlesą tą samą instrukcją 7F:-)
Jonathon Reinhart

17
Nie wspominając już o tym, że optymalizator może modyfikować kod, jeśli rzeczywiście jedna opcja jest szybsza od drugiej.
Elazar Leibovich

3
tylko dlatego, że coś powoduje taką samą liczbę instrukcji niekoniecznie oznacza, że ​​całkowity całkowity czas wykonania wszystkich tych instrukcji będzie taki sam. W rzeczywistości więcej instrukcji można wykonać szybciej. Instrukcje na cykl nie są stałą liczbą, różnią się w zależności od instrukcji.
jontejj

22
@ jontejj Jestem tego bardzo świadomy. Czy w ogóle przeczytałeś moją odpowiedź? Nie podałem nic na temat tej samej liczby instrukcji, stwierdziłem, że są one skompilowane do zasadniczo tych samych instrukcji , z wyjątkiem tego, że jedna instrukcja skoku patrzy na jedną flagę, a druga instrukcja skoku patrzy na dwie flagi. Uważam, że podałem więcej niż wystarczające dowody, aby wykazać, że są one semantycznie identyczne.
Jonathon Reinhart

2
@ jontejj Robisz bardzo dobry punkt. Aby uzyskać jak największą widoczność tej odpowiedzi, prawdopodobnie powinienem trochę ją oczyścić. Dzięki za opinie.
Jonathon Reinhart

593

Historycznie (mówimy o latach 80. i wczesnych 90.) istniało kilka architektur, w których było to prawdą. Podstawowym problemem jest to, że porównanie liczb całkowitych jest z natury implementowane poprzez odejmowanie liczb całkowitych. Powoduje to następujące przypadki.

Comparison     Subtraction
----------     -----------
A < B      --> A - B < 0
A = B      --> A - B = 0
A > B      --> A - B > 0

Teraz, kiedy A < Bodejmowanie musi pożyczyć wysoki bit, aby odejmowanie było poprawne, tak samo jak nosisz i pożyczasz podczas dodawania i odejmowania ręcznie. Ten „pożyczony” bit był zwykle nazywany bitem przenoszenia i byłby testowany przez instrukcję rozgałęzienia. Drugi bit zwany bitem zerowym byłby ustawiony, gdyby odejmowanie było identyczne zero, co sugeruje równość.

Zazwyczaj były co najmniej dwie instrukcje rozgałęzienia warunkowego, jedno do rozgałęzienia na bicie przenoszenia i jedno na bicie zerowym.

Teraz, aby przejść do sedna sprawy, rozszerzmy poprzednią tabelę, aby uwzględnić wyniki przenoszenia i zero bitów.

Comparison     Subtraction  Carry Bit  Zero Bit
----------     -----------  ---------  --------
A < B      --> A - B < 0    0          0
A = B      --> A - B = 0    1          1
A > B      --> A - B > 0    1          0

Tak więc implementacja rozgałęzienia dla A < Bmoże być wykonana w jednej instrukcji, ponieważ bit przenoszenia jest wyraźny tylko w tym przypadku, to znaczy

;; Implementation of "if (A < B) goto address;"
cmp  A, B          ;; compare A to B
bcz  address       ;; Branch if Carry is Zero to the new address

Ale jeśli chcemy dokonać porównania mniejszego lub równego, musimy wykonać dodatkową kontrolę flagi zerowej, aby uchwycić przypadek równości.

;; Implementation of "if (A <= B) goto address;"
cmp A, B           ;; compare A to B
bcz address        ;; branch if A < B
bzs address        ;; also, Branch if the Zero bit is Set

Tak więc na niektórych komputerach użycie porównania „mniej niż” może zaoszczędzić jedną instrukcję maszyny . Było to istotne w erze szybkości procesora poniżej megaherca i stosunku prędkości procesora do pamięci 1: 1, ale dzisiaj jest to prawie zupełnie nieistotne.


10
Dodatkowo architektury takie jak x86 implementują instrukcje takie jak jge, które testują zarówno flagę zero, jak i flagi znak / noś.
greyfade

3
Nawet jeśli dotyczy to danej architektury. Jakie są szanse, że żaden z autorów kompilatorów nigdy nie zauważył, i dodał optymalizację, aby zastąpić wolniejsze szybsze?
Jon Hanna

8
Dotyczy to 8080. Ma instrukcje, aby skoczyć na zero i skoczyć na minus, ale żadna z nich nie może przetestować obu jednocześnie.

4
Dotyczy to również rodziny procesorów 6502 i 65816, która obejmuje także Motorola 68HC11 / 12.
Lucas

31
Nawet w 8080 <=test może być realizowany w jednej instrukcji z wymiany argumentów i testowania dla not <(odpowiednik >=) Jest to pożądane <=z zamienionych argumentów: cmp B,A; bcs addr. Oto
powód,

92

Zakładając, że mówimy o wewnętrznych typach liczb całkowitych, nie ma możliwości, aby jeden był szybszy od drugiego. Są oczywiście semantycznie identyczne. Oboje proszą kompilator, aby zrobił dokładnie to samo. Tylko strasznie uszkodzony kompilator wygenerowałby gorszy kod dla jednego z nich.

Jeśli nie było pewne platforma gdzie <był szybszy niż <=w przypadku prostych typów całkowitych, kompilator powinien zawsze przekształcić <=się <na stałe. Każdy kompilator, który nie byłby po prostu złym kompilatorem (dla tej platformy).


6
+1 Zgadzam się. Nie ma <też <=prędkości, dopóki kompilator nie zdecyduje, jaką prędkość będzie mieć. Jest to bardzo prosta optymalizacja dla kompilatorów, jeśli weźmie się pod uwagę, że generalnie już wykonują optymalizację martwego kodu, optymalizację wywołania ogona, podnoszenie pętli (i czasami rozwijanie), automatyczne równoległe wykonywanie różnych pętli itp. ... Po co marnować czas na rozważanie przedwczesnych optymalizacji ? Uruchom prototyp, profiluj go, aby określić, gdzie leżą najważniejsze optymalizacje, wykonaj te optymalizacje w kolejności według znaczenia i ponownie profiluj po drodze, aby zmierzyć postęp ...
autystyczny

Wciąż istnieją pewne przypadki krawędzi, w których porównanie mające jedną stałą wartość może być wolniejsze w <<, np. Gdy przekształcenie z (a < C)na (a <= C-1)(dla niektórych stałych C) powoduje, Cże trudniej jest zakodować w zestawie instrukcji. Na przykład, zestaw instrukcji może być w stanie reprezentować podpisane stałe od -127 do 128 w zwartej formie w porównaniu, ale stałe poza tym zakresem muszą być ładowane przy użyciu dłuższego, wolniejszego kodowania lub całkowicie innej instrukcji. Tak więc porównanie (a < -127)może nie mieć prostej transformacji.
BeeOnRope

@BeeOnRope Problem nie polegał na tym, czy wykonywanie operacji różniących się z powodu posiadania różnych stałych może wpłynąć na wydajność, ale czy wyrażenie tej samej operacji przy użyciu różnych stałych może wpłynąć na wydajność. Nie porównujemy a > 127tego, a > 128ponieważ nie masz wyboru, korzystasz z tego, czego potrzebujesz. Porównujemy a > 127do a >= 128, które nie mogą wymagać innego kodowania ani różnych instrukcji, ponieważ mają tę samą tabelę prawdy. Każde kodowanie jednego jest równie kodowaniem drugiego.
David Schwartz

I reaguje w sposób ogólny do stwierdzenia, że „Jeśli nie było pewne platforma gdzie [<= wolniej] kompilator zawsze powinny przekształcić <=się <na stałe”. O ile mi wiadomo, ta transformacja obejmuje zmianę stałej. Np. a <= 42Jest kompilowany, a < 43ponieważ <jest szybszy. W niektórych przypadkach taka transformacja nie byłaby owocna, ponieważ nowa stała może wymagać większej lub wolniejszej instrukcji. Oczywiście a > 127i a >= 128są równoważne i kompilator powinien zakodować obie formy w (samo) najszybszy sposób, ale to nie jest sprzeczne z tym, co powiedziałem.
BeeOnRope

67

Widzę, że żadne nie jest szybsze. Kompilator generuje ten sam kod maszynowy dla każdego warunku z inną wartością.

if(a < 901)
cmpl  $900, -4(%rbp)
jg .L2

if(a <=901)
cmpl  $901, -4(%rbp)
jg .L3

Mój przykład ifpochodzi z GCC na platformie x86_64 w systemie Linux.

Autorzy kompilatorów to całkiem sprytni ludzie, którzy myślą o tych rzeczach i wielu innych z nas uważa to za coś oczywistego.

Zauważyłem, że jeśli nie jest to stała, to w obu przypadkach generowany jest ten sam kod maszynowy.

int b;
if(a < b)
cmpl  -4(%rbp), %eax
jge   .L2

if(a <=b)
cmpl  -4(%rbp), %eax
jg .L3

9
Zauważ, że jest to specyficzne dla x86.
Michael Petrotta

10
Myślę, że powinieneś użyć tego, if(a <=900)aby pokazać, że generuje dokładnie taki sam asm :)
Lipis

2
@AdrianCornish Przepraszam .. Zredagowałem go .. jest mniej więcej taki sam .. ale jeśli zmienisz sekundę, jeśli na <= 900, kod asm będzie dokładnie taki sam :) Teraz jest prawie taki sam .. ale ty wiem .. na OCD :)
Lipis

3
@ Boann To może zostać zredukowane do if (prawda) i całkowicie wyeliminowane.
Qsario

5
Nikt nie zauważył, że ta optymalizacja dotyczy tylko ciągłych porównań . Mogę zagwarantować, że nie będzie tak w przypadku porównywania dwóch zmiennych.
Jonathon Reinhart

51

W przypadku kodu zmiennoprzecinkowego porównanie <= może rzeczywiście być wolniejsze (według jednej instrukcji), nawet w nowoczesnych architekturach. Oto pierwsza funkcja:

int compare_strict(double a, double b) { return a < b; }

W PowerPC najpierw wykonuje porównanie zmiennoprzecinkowe (które aktualizuje crrejestr warunków), następnie przenosi rejestr warunków do GPR, przesuwa bit „porównywany mniej niż” na miejsce, a następnie wraca. Wymaga czterech instrukcji.

Teraz rozważ tę funkcję zamiast:

int compare_loose(double a, double b) { return a <= b; }

Wymaga to takiej samej pracy jak compare_strict powyżej, ale teraz są dwa elementy zainteresowania: „był mniejszy niż” i „był równy”. Wymaga to dodatkowej instrukcji ( cror- rejestr bitowy OR), aby połączyć te dwa bity w jeden. compare_looseWymaga więc pięciu instrukcji, a compare_strictczterech.

Możesz pomyśleć, że kompilator może zoptymalizować drugą funkcję w następujący sposób:

int compare_loose(double a, double b) { return ! (a > b); }

Jednak będzie to niepoprawnie obsługiwać NaN. NaN1 <= NaN2i NaN1 > NaN2musimy zarówno ocenić na fałsz.


Na szczęście tak nie działa na x86 (x87). fucomipustawia ZF i CF.
Jonathon Reinhart

3
@JonathonReinhart: Myślę, że nie rozumiesz, co robi PowerPC - rejestr warunków cr jest równoważny z flagami takimi jak ZFi CFna x86. (Chociaż CR jest bardziej elastyczny). To, o czym mówi plakat, przenosi wynik do GPR: który przyjmuje dwie instrukcje na PowerPC, ale x86 ma instrukcję warunkowego ruchu.
Dietrich Epp

@DietrichEpp To, co chciałem dodać po moim oświadczeniu, brzmiało: które możesz następnie natychmiast przeskoczyć na podstawie wartości EFLAGS. Przepraszamy za brak jasności.
Jonathon Reinhart

1
@JonathonReinhart: Tak, możesz także natychmiast skakać na podstawie wartości CR. Odpowiedź nie mówi o skakaniu, skąd pochodzą dodatkowe instrukcje.
Dietrich Epp

34

Być może autor tej nienazwanej książki przeczytał, że a > 0biegnie szybciej a >= 1i uważa, że ​​to prawda.

Ale dzieje się tak dlatego, że a 0jest zaangażowane (ponieważ CMPmoże, w zależności od architektury, zastąpić np. OR), A nie z powodu <.


1
Jasne, w kompilacji „debugowania”, ale działałby zły kompilator, (a >= 1)aby działał wolniej niż (a > 0), ponieważ ten pierwszy może być w prosty sposób przekształcony w drugi przez optymalizator ..
BeeOnRope 16.06.16

2
@BeeOnRope Czasami jestem zaskoczony, jak skomplikowane rzeczy może zoptymalizować optymalizator i jakie proste rzeczy tego nie robi.
glglgl,

1
Rzeczywiście i zawsze warto sprawdzić dane wyjściowe asm dla bardzo niewielu funkcji, w których miałoby to znaczenie. To powiedziawszy, powyższa transformacja jest bardzo podstawowa i była wykonywana nawet w prostych kompilatorach od dziesięcioleci.
BeeOnRope

32

Przynajmniej, gdyby było to prawdą, kompilator mógłby w prosty sposób zoptymalizować <= b do! (A> b), a więc nawet gdyby samo porównanie było w rzeczywistości wolniejsze, przy wszystkich najbardziej naiwnych kompilatorach nie zauważyłbyś różnicy .


Dlaczego! (A> b) jest zoptymalizowaną wersją <= b. Isnt! (A> b) 2 operacje w jednym?
Abhishek Singh,

6
@AbhishekSingh NOTjest po prostu wykonany przez inną instrukcję ( jevs. jne)
Pavel Gatnar

15

Mają tę samą prędkość. Może w jakiejś specjalnej architekturze to, co powiedział, jest słuszne, ale przynajmniej w rodzinie x86 wiem, że są takie same. Ponieważ w tym celu CPU wykona odejmowanie (a - b), a następnie sprawdzi flagi rejestru flag. Dwa bity tego rejestru nazywane są ZF (flaga zerowa) i SF (flaga znaku) i odbywa się to w jednym cyklu, ponieważ zrobi to za pomocą jednej operacji maski.


14

Byłoby to wysoce zależne od podstawowej architektury, do której skompilowano C. Niektóre procesory i architektury mogą mieć wyraźne instrukcje dla równości lub mniejszej i równej, które wykonują się w różnej liczbie cykli.

Byłoby to jednak dość niezwykłe, ponieważ kompilator mógłby go obejść, co czyni go nieistotnym.


1
JEŚLI była różnica w cylindrach. 1) nie byłoby to wykrywalne. 2) Każdy kompilator warty swojej soli już przekształciłby formę wolną w szybszą bez zmiany znaczenia kodu. Zatem posadzona wynikowa instrukcja byłaby identyczna.
Martin York,

Zgadzając się całkowicie, w każdym razie byłaby to całkiem trywialna i głupia różnica. Na pewno nic nie wspominając w książce, która powinna być niezależna od platformy.
Telgin

@lttlrck: Rozumiem. Zajęło mi to trochę czasu (głuptasie). Nie, nie można ich wykryć, ponieważ dzieje się tak wiele innych rzeczy, które uniemożliwiają ich pomiar. Procesor zatrzymuje się / brakuje pamięci podręcznej / sygnałów / zamiany procesów. Zatem w normalnej sytuacji systemu operacyjnego rzeczy na poziomie pojedynczego cyklu nie mogą być mierzalne fizycznie. Jeśli możesz wyeliminować wszystkie te zakłócenia z pomiarów (uruchom go na chipie z wbudowaną pamięcią i bez systemu operacyjnego), nadal masz problem z licznikami, o które musisz się martwić, ale teoretycznie, jeśli uruchomisz go wystarczająco długo, możesz coś zobaczyć.
Martin York,

12

TL; DR odpowiedź

W przypadku większości kombinacji architektury, kompilatora i języka nie będzie to szybsze.

Pełna odpowiedź

Inne odpowiedzi koncentrowały się na architekturze x86 i nie znam architektury ARM (którą wydaje się twój przykładowy asembler) wystarczająco dobrze, aby komentować konkretnie wygenerowany kod, ale jest to przykład mikrooptymalizacji, która jest bardzo architekturą specyficzne i równie prawdopodobne, że będzie antyoptymalizacją, jak optymalizacją .

Jako taki zasugerowałbym, że ten rodzaj mikrooptymalizacji jest przykładem programowania kultowego ładunku, a nie najlepszą praktyką inżynierii oprogramowania.

Prawdopodobnie istnieją pewne architektury, w których jest to optymalizacja, ale znam przynajmniej jedną architekturę, w której może być odwrotnie. Czcigodna architektura Transputera zawierała instrukcje kodu maszynowego tylko dla wartości równej i większej lub równej , więc wszystkie porównania musiały być zbudowane z tych prymitywów.

Nawet wtedy, w prawie wszystkich przypadkach, kompilator mógł zamówić instrukcje oceny w taki sposób, że w praktyce żadne porównanie nie miało żadnej przewagi nad innymi. W najgorszym przypadku może być konieczne dodanie instrukcji odwrotnej (REV), aby zamienić dwie górne pozycje na stosie operandów . Była to instrukcja jednobajtowa, której uruchomienie wymagało jednego cyklu, więc miało najmniejszy możliwy narzut.

To, czy taka mikrooptymalizacja jest optymalizacją czy antyoptymalizacją, zależy od konkretnej architektury, której używasz, więc zazwyczaj złym pomysłem jest przyzwyczajenie się do stosowania mikrooptymalizacji specyficznych dla architektury, w przeciwnym razie instynktownie użyj jednego, gdy jest to niewłaściwe, a wygląda na to, że właśnie to zaleca książka, którą czytasz.


6

Różnica nie powinna być zauważalna, nawet jeśli istnieje. Poza tym w praktyce będziesz musiał wykonać dodatkową akcję a + 1lub a - 1sprawić, by warunek się utrzymał, chyba że będziesz używał magicznych stałych, co jest bardzo złą praktyką pod każdym względem.


1
Jaka jest zła praktyka? Zwiększanie lub zmniejszanie licznika? Jak zatem przechowywać notację indeksową?
jcolebrand

5
To znaczy, jeśli robisz porównanie dwóch typów zmiennych. Oczywiście jest to trywialne, jeśli ustawiasz wartość dla pętli lub czegoś takiego. Ale jeśli masz x <= y, a y jest nieznane, wolniej byłoby go „zoptymalizować” do x <y + 1
JustinDanielson

@JustinDanielson zgodził się. Nie wspominając o brzydkich, mylących itp.
Jonathon Reinhart

4

Można powiedzieć, że wiersz jest poprawny w większości języków skryptowych, ponieważ dodatkowy znak powoduje nieco wolniejsze przetwarzanie kodu. Jednak, jak wskazała najwyższa odpowiedź, nie powinno to mieć żadnego wpływu na C ++, a wszystko, co dzieje się z językiem skryptowym, prawdopodobnie nie dotyczy optymalizacji.


Nieco się nie zgadzam. W programowaniu konkurencyjnym języki skryptowe często oferują najszybsze rozwiązanie problemu, ale aby uzyskać prawidłowe rozwiązanie, należy zastosować prawidłowe techniki (czytaj: optymalizacja).
Tyler Crompton,

3

Kiedy pisałem tę odpowiedź, szukałem tylko w tytule pytanie o <vs <w ogóle, a nie na przykład specyficzna stałej a < 901vs. a <= 900. Wiele kompilatorów zawsze zmniejsza wielkość stałych poprzez konwersję między <i <=, np. Ponieważ operand bezpośredni x86 ma krótsze 1-bajtowe kodowanie dla -128..127.

W przypadku ARM, a zwłaszcza AArch64, możliwość kodowania w trybie natychmiastowym zależy od możliwości obrócenia wąskiego pola w dowolne miejsce w słowie. Więc cmp w0, #0x00f000byłoby encodeable, natomiast cmp w0, #0x00effffnie może być. Zatem reguła make-it-mniejsza dla porównania vs. stała czasowa kompilacji nie zawsze dotyczy AArch64.


<vs. <= ogólnie, w tym dla warunków zmiennych w czasie wykonywania

W języku asemblera na większości maszyn porównanie dla <=ma taki sam koszt jak dla porównania <. Odnosi się to do tego, czy rozgałęziasz się na nim, booleanizujesz go, aby utworzyć liczbę całkowitą 0/1, czy też używasz go jako predykatu dla operacji wybierania bez rozgałęzień (takiej jak x86 CMOV). Pozostałe odpowiedzi dotyczyły tylko tej części pytania.

Ale to pytanie dotyczy operatorów C ++, danych wejściowych do optymalizatora. Zwykle oba są równie wydajne; rada zawarta w książce wydaje się całkowicie nieprawdziwa, ponieważ kompilatory zawsze mogą przekształcić porównania, które implementują w asm. Ale istnieje co najmniej jeden wyjątek, w którym użycie <=może przypadkowo stworzyć coś, czego kompilator nie może zoptymalizować.

Jako stan pętli, istnieją przypadki, gdy <=jest jakościowo różna od <gdy zatrzymuje kompilator dowodzi, że pętla jest nieskończona. Może to mieć dużą różnicę, wyłączając automatyczną wektoryzację.

Niepodpisane przepełnienie jest dobrze zdefiniowane jako zawijanie bazy-2, w przeciwieństwie do podpisanego przepełnienia (UB). Liczniki pętli podpisanych są na ogół bezpieczne przed kompilatorami, które optymalizują się w oparciu o UB z przepełnieniem podpisu: ++i <= sizenie zawsze: w końcu stanie się fałszywy. ( Co każdy programista C powinien wiedzieć o nieokreślonym zachowaniu )

void foo(unsigned size) {
    unsigned upper_bound = size - 1;  // or any calculation that could produce UINT_MAX
    for(unsigned i=0 ; i <= upper_bound ; i++)
        ...

Kompilatory mogą optymalizować tylko w taki sposób, aby zachować (zdefiniowane i prawnie obserwowalne) zachowanie źródła C ++ dla wszystkich możliwych wartości wejściowych , z wyjątkiem tych, które prowadzą do nieokreślonego zachowania.

(Prosty i <= sizerównież stworzyłby problem, ale pomyślałem, że obliczenie górnej granicy było bardziej realistycznym przykładem przypadkowego wprowadzenia możliwości nieskończonej pętli dla danych wejściowych, których nie obchodzi, ale które kompilator musi wziąć pod uwagę.)

W takim przypadku size=0prowadzi do upper_bound=UINT_MAXi i <= UINT_MAXzawsze jest prawdą. Ta pętla jest więc nieskończona size=0, a kompilator musi to szanować, nawet jeśli jako programista prawdopodobnie nigdy nie zamierzasz przekazać size = 0. Jeśli kompilator może wprowadzić tę funkcję do programu wywołującego, w którym może udowodnić, że rozmiar = 0 jest niemożliwy, to świetnie, może zoptymalizować tak, jak mógłby i < size.

Asm like if(!size) skip the loop; do{...}while(--size);to jeden z normalnie wydajnych sposobów optymalizacji for( i<size )pętli, jeśli rzeczywista wartość inie jest potrzebna w pętli ( dlaczego pętle są zawsze kompilowane w stylu „do ... while” (skok z tyłu)? ).

Ale to robi {}, chociaż nie może być nieskończone: jeśli zostanie wprowadzone size==0, otrzymamy 2 ^ iteracje. ( Iteracja po wszystkich liczbach całkowitych bez znaku w pętli for C umożliwia wyrażenie pętli nad wszystkimi liczbami całkowitymi bez znaku, w tym zerem, ale nie jest to łatwe bez flagi przeniesienia tak, jak w asm.)

Ponieważ możliwe jest obejście licznika pętli, nowoczesne kompilatory często po prostu „poddają się” i nie optymalizują prawie tak agresywnie.

Przykład: suma liczb całkowitych od 1 do n

Korzystanie i <= nz rozpoznawania idiomów klanu bez znaku , który optymalizuje sum(1 .. n)pętle z zamkniętą formą opartą na n * (n+1) / 2formule Gaussa .

unsigned sum_1_to_n_finite(unsigned n) {
    unsigned total = 0;
    for (unsigned i = 0 ; i < n+1 ; ++i)
        total += i;
    return total;
}

x86-64 asm z clang7.0 i gcc8.2 w eksploratorze kompilatorów Godbolt

 # clang7.0 -O3 closed-form
    cmp     edi, -1       # n passed in EDI: x86-64 System V calling convention
    je      .LBB1_1       # if (n == UINT_MAX) return 0;  // C++ loop runs 0 times
          # else fall through into the closed-form calc
    mov     ecx, edi         # zero-extend n into RCX
    lea     eax, [rdi - 1]   # n-1
    imul    rax, rcx         # n * (n-1)             # 64-bit
    shr     rax              # n * (n-1) / 2
    add     eax, edi         # n + (stuff / 2) = n * (n+1) / 2   # truncated to 32-bit
    ret          # computed without possible overflow of the product before right shifting
.LBB1_1:
    xor     eax, eax
    ret

Ale w przypadku wersji naiwnej po prostu dostajemy głupią pętlę od clang.

unsigned sum_1_to_n_naive(unsigned n) {
    unsigned total = 0;
    for (unsigned i = 0 ; i<=n ; ++i)
        total += i;
    return total;
}
# clang7.0 -O3
sum_1_to_n(unsigned int):
    xor     ecx, ecx           # i = 0
    xor     eax, eax           # retval = 0
.LBB0_1:                       # do {
    add     eax, ecx             # retval += i
    add     ecx, 1               # ++1
    cmp     ecx, edi
    jbe     .LBB0_1            # } while( i<n );
    ret

GCC nie używa formy zamkniętej w żaden sposób, więc wybór warunku pętli tak naprawdę nie zaszkodzi ; automatycznie wektoryzuje z dodaniem liczb całkowitych SIMD, uruchamiając 4 iwartości równolegle w elementach rejestru XMM.

# "naive" inner loop
.L3:
    add     eax, 1       # do {
    paddd   xmm0, xmm1    # vect_total_4.6, vect_vec_iv_.5
    paddd   xmm1, xmm2    # vect_vec_iv_.5, tmp114
    cmp     edx, eax      # bnd.1, ivtmp.14     # bound and induction-variable tmp, I think.
    ja      .L3 #,       # }while( n > i )

 "finite" inner loop
  # before the loop:
  # xmm0 = 0 = totals
  # xmm1 = {0,1,2,3} = i
  # xmm2 = set1_epi32(4)
 .L13:                # do {
    add     eax, 1       # i++
    paddd   xmm0, xmm1    # total[0..3] += i[0..3]
    paddd   xmm1, xmm2    # i[0..3] += 4
    cmp     eax, edx
    jne     .L13      # }while( i != upper_limit );

     then horizontal sum xmm0
     and peeled cleanup for the last n%3 iterations, or something.

Ma również zwykłą pętlę skalarną, która, jak sądzę, wykorzystuje do bardzo małych ni / lub do nieskończonej pętli.

BTW, obie te pętle marnują instrukcję (i kupę w procesorach z rodziny Sandybridge) narzutu pętli. sub eax,1/ jnzzamiast add eax,1/ cmp / jcc byłoby bardziej wydajne. 1 uop zamiast 2 (po makro-fuzji sub / jcc lub cmp / jcc). Kod po obu pętlach bezwarunkowo zapisuje EAX, więc nie używa końcowej wartości licznika pętli.


Ładny wymyślony przykład. A co z twoim innym komentarzem na temat potencjalnego wpływu na realizację poza zamówieniem z powodu użycia EFLAGS? Czy jest to czysto teoretyczne, czy może faktycznie zdarza się, że JB prowadzi do lepszego rurociągu niż JBE?
rustyx

@rustyx: czy skomentowałem to gdzieś pod inną odpowiedzią? Kompilatory nie będą emitować kodu, który powoduje przeciągnięcia z częściową flagą, a na pewno nie dla C <lub <=. Ale na pewno test ecx,ecx/ bt eax, 3/ jbeprzeskoczy, jeśli ustawiony jest ZF (ecx == 0) lub jeśli ustawiony jest CF (bit 3 EAX == 1), powodując częściowe zatrzymanie flagi na większości procesorów, ponieważ czytane flagi nie wszystkie pochodzą z ostatniej instrukcji, aby napisać dowolne flagi. W przypadku rodziny Sandybridge tak naprawdę nie zwleka, wystarczy wstawić scalający się element. cmp/ testnapisz wszystkie flagi, ale btpozostawia ZF niezmodyfikowany. felixcloutier.com/x86/bt
Peter Cordes

2

Tylko jeśli ludzie, którzy stworzyli komputery, mają złe logiki boolowskie. Które nie powinny być.

Każde porównanie ( >= <= > <) można wykonać z tą samą prędkością.

Każde porównanie to tylko odjęcie (różnica) i sprawdzenie, czy jest dodatnie / ujemne.
(Jeśli msbjest ustawiony, liczba jest ujemna)

Jak sprawdzić a >= b? Sub a-b >= 0Sprawdź, czy a-bjest dodatnia.
Jak sprawdzić a <= b? Sub 0 <= b-aSprawdź, czy b-ajest dodatnia.
Jak sprawdzić a < b? Sub a-b < 0Sprawdź, czy a-bjest ujemna.
Jak sprawdzić a > b? Sub 0 > b-aSprawdź, czy b-ajest ujemna.

Mówiąc najprościej, komputer może po prostu zrobić to pod maską dla danej operacji:

a >= b== msb(a-b)==0
a <= b== msb(b-a)==0
a > b== msb(b-a)==1
a < b==msb(a-b)==1

i oczywiście komputer nie będzie faktycznie trzeba zrobić ==0albo ==1albo.
bo ==0może po prostu odwrócić msbobwód.

W każdym razie z pewnością nie a >= bzostałyby obliczone jako a>b || a==blol

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.