<È più veloce di <=?


1574

È if( a < 901 )più veloce di if( a <= 900 ).

Non esattamente come in questo semplice esempio, ma ci sono lievi variazioni delle prestazioni nel codice complesso del loop. Suppongo che questo debba fare qualcosa con il codice macchina generato nel caso sia anche vero.


153
Non vedo alcun motivo per cui questa domanda debba essere chiusa (e soprattutto non cancellata, come mostrano attualmente i voti) dato il suo significato storico, la qualità della risposta e il fatto che le altre domande principali in termini di prestazioni rimangano aperte. Al massimo dovrebbe essere bloccato. Inoltre, anche se la domanda stessa è male informata / ingenua, il fatto che sia apparsa in un libro significa che la disinformazione originale esiste là fuori in fonti "credibili" da qualche parte, e questa domanda è quindi costruttiva in quanto aiuta a chiarire ciò.
Jason C

32
Non ci hai mai detto a quale libro ti riferisci.
Jonathon Reinhart,

160
La digitazione <è due volte più veloce della digitazione <=.
Deqing,

6
Era vero sull'8086.
Giosuè il

7
Il numero di voti evidenzia chiaramente che ci sono centinaia di persone che hanno eccessivamente ottimizzato.
m93a,

Risposte:


1704

No, non sarà più veloce sulla maggior parte delle architetture. Non hai specificato, ma su x86, tutti i confronti integrali verranno generalmente implementati in due istruzioni macchina:

  • A testo cmpistruzione, che impostaEFLAGS
  • E Jccun'istruzione (salta) , a seconda del tipo di confronto (e del layout del codice):
    • jne - Salta se non uguale -> ZF = 0
    • jz - Salta se zero (uguale) -> ZF = 1
    • jg - Salta se maggiore -> ZF = 0 and SF = OF
    • (eccetera...)

Esempio (modificato per brevità) Compilato con$ gcc -m32 -S -masm=intel test.c

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

Compila per:

    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:

E

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

Compila per:

    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:

Quindi l'unica differenza tra i due è jgcontro jgeun'istruzione. I due impiegheranno lo stesso tempo.


Vorrei rispondere al commento secondo cui nulla indica che le diverse istruzioni di salto richiedono lo stesso tempo. Questo è un po 'difficile da rispondere, ma ecco cosa posso dare: nel Intel Instruction Set Reference , sono tutti raggruppati sotto un'unica istruzione, Jcc(Salta se la condizione è soddisfatta). Lo stesso raggruppamento viene creato insieme nel Manuale di riferimento sull'ottimizzazione , nell'Appendice C. Latenza e produttività.

Latenza : il numero di cicli di clock necessari affinché il nucleo di esecuzione completi l'esecuzione di tutti i μops che formano un'istruzione.

Throughput : il numero di cicli di clock necessari per attendere prima che le porte di emissione siano libere di accettare nuovamente la stessa istruzione. Per molte istruzioni, il throughput di un'istruzione può essere significativamente inferiore alla sua latenza

I valori per Jccsono:

      Latency   Throughput
Jcc     N/A        0.5

con la seguente nota a piè di pagina su Jcc:

7) La selezione delle istruzioni di salto condizionato dovrebbe basarsi sulla raccomandazione della sezione 3.4.1, "Ottimizzazione della previsione dei rami", per migliorare la prevedibilità dei rami. Quando i rami vengono previsti correttamente, la latenza di jccè effettivamente zero.

Pertanto, nulla nei documenti Intel tratta mai Jccun'istruzione in modo diverso dalle altre.

Se si pensa al circuito effettivo utilizzato per implementare le istruzioni, si può presumere che ci sarebbero semplici porte AND / OR sui diversi bit in EFLAGS, per determinare se le condizioni sono soddisfatte. Non vi è quindi alcun motivo per cui un'istruzione che verifica due bit dovrebbe richiedere più o meno tempo di una prova solo una (Ignorare il ritardo di propagazione del gate, che è molto inferiore al periodo di clock).


Modifica: virgola mobile

Questo vale anche per il punto mobile x87: (Praticamente lo stesso codice di cui sopra, ma con doubleinvece di 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 in realtà jge jnlesono le stesse istruzioni, 7F:-)
Jonathon Reinhart il

17
Per non parlare del fatto che l'ottimizzatore può modificare il codice se effettivamente un'opzione è più veloce dell'altra.
Elazar Leibovich,

3
solo perché qualcosa risulta nella stessa quantità di istruzioni non significa necessariamente che il tempo totale totale dell'esecuzione di tutte quelle istruzioni sarà lo stesso. In realtà più istruzioni potrebbero essere eseguite più velocemente. Le istruzioni per ciclo non sono un numero fisso, variano a seconda delle istruzioni.
jontejj

22
@jontejj Ne sono consapevole. Hai persino letto la mia risposta? Non ho affermato nulla sullo stesso numero di istruzioni, ho affermato che sono state compilate essenzialmente con gli stessi identici istruzioni , tranne per il fatto che un'istruzione di salto sta guardando una bandiera e l'altra istruzione di salto sta guardando due bandiere. Credo di aver fornito prove più che adeguate per dimostrare che sono semanticamente identiche.
Jonathon Reinhart,

2
@jontejj Hai fatto un'ottima osservazione. Per tutta la visibilità che questa risposta ottiene, dovrei probabilmente dargli un po 'di pulizia. Grazie per il feedback.
Jonathon Reinhart,

593

Storicamente (stiamo parlando degli anni '80 e dei primi anni '90), c'erano alcune architetture in cui questo era vero. Il problema alla radice è che il confronto di numeri interi è intrinsecamente implementato tramite sottrazioni di numeri interi. Ciò dà origine ai seguenti casi.

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

Ora, quando A < Bla sottrazione deve prendere in prestito un bit alto perché la sottrazione sia corretta, proprio come si porta e si prende in prestito quando si aggiunge e sottrae a mano. Questo bit "preso in prestito" era di solito indicato come bit di riporto e sarebbe testabile da un'istruzione di diramazione. Un secondo bit chiamato bit zero verrebbe impostato se la sottrazione fosse identicamente zero, il che implicava l'uguaglianza.

Di solito c'erano almeno due istruzioni di diramazione condizionate, una da diramare sul bit di riporto e una sul bit di zero.

Ora, per arrivare al nocciolo della questione, espandiamo la tabella precedente per includere i risultati carry e zero bit.

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

Pertanto, l'implementazione di una diramazione per A < Bpuò essere eseguita in un'unica istruzione, poiché il bit di riporto è chiaro solo in questo caso, ovvero

;; 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

Ma, se vogliamo fare un confronto minore o uguale, dobbiamo fare un ulteriore controllo della bandiera zero per cogliere il caso dell'uguaglianza.

;; 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

Pertanto, su alcune macchine, l'utilizzo di un confronto "minore di" potrebbe salvare un'istruzione di macchina . Ciò era rilevante nell'era della velocità del processore sub-megahertz e dei rapporti di velocità CPU-memoria 1: 1, ma oggi è quasi del tutto irrilevante.


10
Inoltre, architetture come x86 implementano istruzioni simili jge, che testano sia i flag zero che sign / carry.
Greyfade,

3
Anche se è vero per una determinata architettura. Quali sono le probabilità che nessuno degli autori del compilatore abbia mai notato e ha aggiunto un'ottimizzazione per sostituire il più lento con il più veloce?
Jon Hanna,

8
Questo è vero sull'8080. Ha le istruzioni per saltare su zero e saltare su meno, ma nessuno che può testare entrambi contemporaneamente.

4
Questo è anche il caso della famiglia di processori 6502 e 65816, che si estende anche al Motorola 68HC11 / 12.
Lucas,

31
Anche sulla 8080 un <=test può essere implementato in un'istruzione con scambiando gli operandi e test per (a equivalente ) Questo è il desiderato con operandi invertiti: . Questo è il motivo per cui questo test è stato omesso da Intel, lo consideravano ridondante e non potevi permetterti istruzioni ridondanti in quei momenti :-)not <>=<=cmp B,A; bcs addr
Gunther Piez,

92

Supponendo che stiamo parlando di tipi interi interni, non è possibile che uno sia più veloce dell'altro. Sono ovviamente semanticamente identici. Entrambi chiedono al compilatore di fare esattamente la stessa cosa. Solo un compilatore orribilmente rotto genererebbe un codice inferiore per uno di questi.

Se c'era una piattaforma in cui <era più veloce rispetto <=ai tipi interi semplici, il compilatore dovrebbe sempre convertirsi <=in <costanti. Qualsiasi compilatore che non lo fosse sarebbe solo un cattivo compilatore (per quella piattaforma).


6
+1 Sono d'accordo. Né <, né <=avere la velocità fino a quando il compilatore decide quale velocità avranno. Questa è un'ottimizzazione molto semplice per i compilatori se si considera che generalmente eseguono già l'ottimizzazione del codice morto, l'ottimizzazione delle chiamate in coda, il sollevamento dei loop (e lo svolgimento, in occasioni), la parallelizzazione automatica di vari loop, ecc ... Perché perdere tempo a ponderare ottimizzazioni premature ? Ottieni un prototipo in esecuzione, profilalo per determinare dove si trovano le ottimizzazioni più significative, esegui quelle ottimizzazioni in ordine di significato e profila di nuovo lungo il percorso per misurare i progressi ...
autistico,

Ci sono ancora alcuni casi limite in cui un comparatore avente un valore costante potrebbe essere inferiore in <=, ad esempio, quando la trasformazione da (a < C)a (a <= C-1)(per una costante C) provoca Cessere più difficile da codificare nel set di istruzioni. Ad esempio, un set di istruzioni può essere in grado di rappresentare costanti con segno da -127 a 128 in una forma compatta in confronto, ma le costanti al di fuori di tale intervallo devono essere caricate utilizzando una codifica più lunga e più lenta o un'altra istruzione interamente. Quindi un confronto come (a < -127)potrebbe non avere una trasformazione semplice.
BeeOnRope,

@BeeOnRope Il problema non era se l'esecuzione di operazioni diverse a causa della presenza di costanti diverse potesse influire sulle prestazioni, ma se esprimere la stessa operazione utilizzando costanti diverse potesse influire sulle prestazioni. Quindi non stiamo confrontando a > 127per a > 128perché non hai scelta là, si utilizza quello che vi serve. Ci stiamo confrontando a > 127per a >= 128, che non può richiedere codifica diversa o diverse istruzioni perché hanno la stessa tabella di verità. Qualsiasi codifica di uno è ugualmente una codifica dell'altro.
David Schwartz,

Stavo rispondendo in generale alla tua affermazione che "Se esistesse una piattaforma in cui [<= era più lento] il compilatore dovrebbe sempre convertirsi <=in <costanti". Per quanto ne so, quella trasformazione implica il cambiamento della costante. Ad esempio, a <= 42viene compilato come a < 43perché <è più veloce. In alcuni casi limite, tale trasformazione non sarebbe fruttuosa perché la nuova costante potrebbe richiedere istruzioni più o più lente. Certamente a > 127e a >= 128sono equivalenti e un compilatore dovrebbe codificare entrambi i moduli nella (stessa) maniera più veloce, ma ciò non è in contrasto con quello che ho detto.
BeeOnRope,

67

Vedo che nessuno dei due è più veloce. Il compilatore genera lo stesso codice macchina in ogni condizione con un valore diverso.

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

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

Il mio esempio ifè di GCC su piattaforma x86_64 su Linux.

Gli autori di compilatori sono persone piuttosto intelligenti e pensano a queste cose e molti altri la danno per scontato.

Ho notato che se non è una costante, in entrambi i casi viene generato lo stesso codice macchina.

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

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

9
Si noti che questo è specifico per x86.
Michael Petrotta,

10
Penso che dovresti usarlo if(a <=900)per dimostrare che genera esattamente la stessa cosa :)
Lipis,

2
@AdrianCornish Mi dispiace .. l'ho modificato .. è più o meno lo stesso .. ma se cambi il secondo se in <= 900 il codice asm sarà esattamente lo stesso :) Ora è praticamente lo stesso .. ma tu so .. per il DOC :)
Lipis,

3
@Boann Potrebbe ridursi a (vero) ed essere eliminato completamente.
Qsario,

5
Nessuno ha sottolineato che questa ottimizzazione si applica solo a confronti costanti . Posso garantire che non sarà fatto in questo modo per confrontare due variabili.
Jonathon Reinhart,

51

Per il codice a virgola mobile, il confronto <= può effettivamente essere più lento (di un'istruzione) anche su architetture moderne. Ecco la prima funzione:

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

Su PowerPC, dapprima questo esegue un confronto in virgola mobile (che aggiorna cr, il registro delle condizioni), quindi sposta il registro delle condizioni su un GPR, sposta in posizione il bit "confrontato meno di" e quindi ritorna. Sono necessarie quattro istruzioni.

Ora considera invece questa funzione:

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

Ciò richiede lo stesso lavoro di cui compare_strictsopra, ma ora ci sono due parti di interesse: "era meno di" e "era uguale a". Ciò richiede un'istruzione aggiuntiva ( cror- condition register bitwise OR) per combinare questi due bit in uno. Quindi compare_looserichiede cinque istruzioni, mentre compare_strictrichiede quattro.

Potresti pensare che il compilatore potrebbe ottimizzare la seconda funzione in questo modo:

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

Tuttavia, questo gestirà in modo errato NaNs. NaN1 <= NaN2e NaN1 > NaN2entrambi devono valutare false.


Fortunatamente non funziona così su x86 (x87). fucomipimposta ZF e CF.
Jonathon Reinhart,

3
@JonathonReinhart: Penso che tu stia fraintendendo cosa stia facendo il PowerPC - il registro delle condizioni cr è l'equivalente di flag come ZFe CFsu x86. (Sebbene il CR sia più flessibile.) Ciò di cui parla il poster è spostare il risultato in un GPR: che accetta due istruzioni su PowerPC, ma x86 ha un'istruzione di spostamento condizionale.
Dietrich Epp,

@DietrichEpp Quello che intendevo aggiungere dopo la mia affermazione era: che puoi immediatamente saltare in base al valore di EFLAGS. Scusa se non sono stato chiaro.
Jonathon Reinhart,

1
@JonathonReinhart: Sì, e puoi anche saltare immediatamente in base al valore del CR. La risposta non sta parlando di saltare, da dove provengono le istruzioni extra.
Dietrich Epp,

34

Forse l'autore di quel libro senza nome ha letto che a > 0corre più veloce di a >= 1e pensa che sia vero universalmente.

Ma è perché a 0è coinvolto (perché CMPpuò, a seconda dell'architettura, sostituito ad esempio con OR) e non a causa di <.


1
Certo, in una build di "debug", ma ci vorrebbe un cattivo compilatore per (a >= 1)funzionare più lentamente di (a > 0), poiché il primo può essere banalmente trasformato in quest'ultimo
dall'ottimizzatore

2
@BeeOnRope A volte sono sorpreso delle cose complicate che un ottimizzatore può ottimizzare e su quali cose semplici non riesce a farlo.
glglgl,

1
Anzi, e vale sempre la pena controllare l'output asm per le pochissime funzioni in cui sarebbe importante. Detto questo, la trasformazione di cui sopra è molto semplice ed è stata eseguita anche in compilatori semplici per decenni.
BeeOnRope,

32

Per lo meno, se questo fosse vero, un compilatore potrebbe banalmente ottimizzare a <= b to! (A> b), e quindi anche se il confronto stesso fosse effettivamente più lento, con tutti tranne il compilatore più ingenuo non noteresti una differenza .


Perché! (A> b) è la versione ottimizzata di a <= b. Non è! (A> b) 2 operazione in una?
Abhishek Singh,

6
@AbhishekSingh NOTè stato appena creato da altre istruzioni ( jevs. jne)
Pavel Gatnar

15

Hanno la stessa velocità. Forse in qualche architettura speciale quello che ha detto è giusto, ma nella famiglia x86 almeno so che sono uguali. Perché per fare ciò la CPU farà una sottostrazione (a - b) e quindi controllerà i flag del registro flag. Due bit di quel registro sono chiamati ZF (zero flag) e SF (sign flag), ed è fatto in un ciclo, perché lo farà con una maschera.


14

Ciò sarebbe fortemente dipendente dall'architettura sottostante alla quale è compilata la C. Alcuni processori e architetture potrebbero avere istruzioni esplicite per uguale o inferiore o uguale a, che vengono eseguite in diversi numeri di cicli.

Sarebbe piuttosto insolito, dato che il compilatore potrebbe aggirarlo, rendendolo irrilevante.


1
SE c'era una differenza negli stili. 1) non sarebbe rilevabile. 2) Qualsiasi compilatore degno di questo nome avrebbe già effettuato la trasformazione dalla forma lenta alla forma più veloce senza modificare il significato del codice. Quindi l'istruzione risultante piantata sarebbe identica.
Martin York,

D'accordo, sarebbe comunque una differenza piuttosto banale e sciocca. Certamente nulla da menzionare in un libro che dovrebbe essere indipendente dalla piattaforma.
Telgin,

@lttlrck: ho capito. Mi ci è voluto un po '(sciocco). No, non sono rilevabili perché ci sono così tante altre cose che rendono impossibile la loro misurazione. Stalli processore / cache mancati / segnali / scambio di processo. Pertanto, in una normale situazione del sistema operativo, le cose a livello di singolo ciclo non possono essere misurabili fisicamente. Se riesci a eliminare tutte queste interferenze dalle tue misurazioni (eseguile su un chip con memoria integrata e nessun sistema operativo), avrai comunque la granularità dei tuoi timer di cui preoccuparti, ma teoricamente se lo esegui abbastanza a lungo potresti vedere qualcosa.
Martin York,

12

TL; risposta DR

Per la maggior parte delle combinazioni di architettura, compilatore e linguaggio non sarà più veloce.

Risposta completa

Altre risposte si sono concentrate sull'architettura x86 e non conosco l' architettura ARM (che sembra essere il tuo esempio assembler) abbastanza bene da commentare specificamente il codice generato, ma questo è un esempio di micro-ottimizzazione che è molto architettura specifico ed è probabile che sia un'anti-ottimizzazione in quanto deve essere un'ottimizzazione .

Pertanto, suggerirei che questo tipo di micro-ottimizzazione sia un esempio di programmazione di culto del carico piuttosto che la migliore pratica di ingegneria del software.

Probabilmente ci sono alcune architetture in cui si tratta di un'ottimizzazione, ma conosco almeno un'architettura in cui potrebbe essere vero il contrario. La venerabile architettura Transputer aveva solo istruzioni di codice macchina uguali e maggiori o uguali a , quindi tutti i confronti dovevano essere costruiti da queste primitive.

Anche allora, in quasi tutti i casi, il compilatore poteva ordinare le istruzioni di valutazione in modo tale che, in pratica, nessun confronto avesse alcun vantaggio rispetto a nessun altro. Nel peggiore dei casi, potrebbe essere necessario aggiungere un'istruzione inversa (REV) per scambiare i primi due elementi nello stack dell'operando . Si trattava di un'istruzione a byte singolo che richiedeva l'esecuzione di un singolo ciclo, quindi il sovraccarico minimo era possibile.

Che una micro-ottimizzazione come questa sia un'ottimizzazione o un'anti-ottimizzazione dipende dall'architettura specifica che stai usando, quindi di solito è una cattiva idea prendere l'abitudine di usare micro-ottimizzazioni specifiche dell'architettura, altrimenti potresti istintivamente usane uno quando è inappropriato farlo, e sembra che questo sia esattamente ciò che il libro che stai leggendo sta sostenendo.


6

Non dovresti essere in grado di notare la differenza anche se ce n'è. Inoltre, in pratica, dovrai fare un ulteriore a + 1o a - 1far valere la condizione a meno che tu non abbia intenzione di usare alcune costanti magiche, il che è una cattiva pratica a tutti i costi.


1
Qual è la cattiva pratica? Incrementare o decrementare un contatore? Come memorizzi la notazione dell'indice allora?
jcolebrand,

5
Significa che se stai confrontando 2 tipi di variabili. Ovviamente è banale se stai impostando il valore per un loop o qualcosa del genere. Ma se hai x <= ye y è sconosciuto, sarebbe più lento 'ottimizzarlo' su x <y + 1
JustinDanielson

@JustinDanielson concordato. Per non parlare di brutti, confusi, ecc.
Jonathon Reinhart,

4

Si potrebbe dire che la linea è corretta nella maggior parte dei linguaggi di scripting, poiché il carattere in più comporta una elaborazione del codice leggermente più lenta. Tuttavia, come sottolineato dalla risposta principale, non dovrebbe avere alcun effetto in C ++ e probabilmente qualsiasi cosa venga eseguita con un linguaggio di scripting non è così preoccupata per l'ottimizzazione.


Non sono d'accordo. Nella programmazione competitiva, i linguaggi di scripting offrono spesso la soluzione più rapida a un problema, ma per ottenere una soluzione corretta è necessario applicare le tecniche corrette (leggi: ottimizzazione).
Tyler Crompton,

3

Quando ho scritto questa risposta, stavo solo guardando il titolo della domanda su <vs. <= in generale, non l'esempio specifico di una costante a < 901contro a <= 900. Molti compilatori riducono sempre l'entità delle costanti convertendo tra <e <=, ad esempio perché l'operando immediato x86 ha una codifica a 1 byte più corta per -128..127.

Per ARM e in particolare per AArch64, essere in grado di codificare come immediato dipende dalla possibilità di ruotare un campo stretto in qualsiasi posizione in una parola. Quindi cmp w0, #0x00f000sarebbe codificabile, mentre cmp w0, #0x00effffpotrebbe non esserlo. Quindi la regola make-it-small per il confronto rispetto a una costante di compilazione non si applica sempre per AArch64.


<vs. <= in generale, anche per condizioni variabili di runtime

Nel linguaggio assembly della maggior parte delle macchine, un confronto per <=ha lo stesso costo di un confronto per <. Questo vale sia che si stia ramificando su di esso, che lo si booleanti per creare un numero intero 0/1, sia che si usi come predicato per un'operazione di selezione senza rami (come x86 CMOV). Le altre risposte hanno affrontato solo questa parte della domanda.

Ma questa domanda riguarda gli operatori C ++, l' input per l'ottimizzatore. Normalmente sono entrambi ugualmente efficienti; i consigli del libro sembrano totalmente falsi perché i compilatori possono sempre trasformare il confronto che implementano in asm. Ma c'è almeno un'eccezione in cui l'utilizzo <=può creare accidentalmente qualcosa che il compilatore non può ottimizzare.

Come condizione di loop, ci sono casi in cui <=è qualitativamente diverso da <, quando impedisce al compilatore di dimostrare che un loop non è infinito. Questo può fare una grande differenza, disabilitando l'auto-vettorializzazione.

L'overflow senza segno è ben definito come avvolgimento base-2, a differenza dell'overflow con segno (UB). I contatori di loop firmati sono generalmente al sicuro da ciò con i compilatori che si ottimizzano in base al superamento dell'UB non firmato: ++i <= sizealla fine diventeranno sempre falsi. ( Ciò che ogni programmatore C dovrebbe sapere sul comportamento indefinito )

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++)
        ...

I compilatori possono ottimizzare solo in modo da preservare il comportamento (definito e legalmente osservabile) della sorgente C ++ per tutti i possibili valori di input , ad eccezione di quelli che portano a comportamenti indefiniti.

(Un semplice i <= sizecreerebbe anche il problema, ma ho pensato che calcolare un limite superiore fosse un esempio più realistico dell'introduzione accidentale della possibilità di un ciclo infinito per un input che non ti interessa ma che il compilatore deve considerare.)

In questo caso, size=0porta a upper_bound=UINT_MAXed i <= UINT_MAXè sempre vero. Quindi questo ciclo è infinito size=0e il compilatore deve rispettarlo, anche se probabilmente il programmatore non intende mai passare size = 0. Se il compilatore può incorporare questa funzione in un chiamante in cui può dimostrare che size = 0 è impossibile, quindi ottimo, può ottimizzare come potrebbe i < size.

Assomiglia if(!size) skip the loop; do{...}while(--size);è un modo normalmente efficiente per ottimizzare un for( i<size )loop, se il valore effettivo di inon è necessario all'interno del loop ( Perché i loop sono sempre compilati nello stile "do ... while" (tail jump)? ).

Ma ciò {} mentre non può essere infinito: se inserito con size==0, otteniamo 2 ^ n iterazioni. (L' iterazione su tutti i numeri interi senza segno in un ciclo for C rende possibile esprimere un ciclo su tutti i numeri interi senza segno incluso lo zero, ma non è facile senza un flag carry come è in asm.)

Con una possibilità avvolgente del loop loop, i compilatori moderni spesso "rinunciano" e non ottimizzano in modo altrettanto aggressivo.

Esempio: somma di numeri interi da 1 a n

Usando le i <= nsconfitte non firmate il riconoscimento del linguaggio di clang che ottimizza i sum(1 .. n)loop con una forma chiusa basata sulla n * (n+1) / 2formula di Gauss .

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 da clang7.0 e gcc8.2 sull'esploratore del compilatore 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

Ma per la versione ingenua, abbiamo appena ottenuto un loop stupido da 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 non usa una forma chiusa in entrambi i modi, quindi la scelta della condizione del loop non la danneggia davvero ; si auto-vettorializza con l'aggiunta di numeri interi SIMD, eseguendo 4 ivalori in parallelo negli elementi di un registro 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.

Ha anche un semplice ciclo scalare che penso che usi per ncasi di loop molto piccoli e / o infiniti.

A proposito, entrambi questi loop sprecano un'istruzione (e un up sulle CPU della famiglia Sandybridge) sull'overhead del loop. sub eax,1/ jnzinvece di add eax,1/ cmp / jcc sarebbe più efficiente. 1 uop invece di 2 (dopo macrofusione di sub / jcc o cmp / jcc). Il codice dopo entrambi i loop scrive EAX incondizionatamente, quindi non utilizza il valore finale del contatore loop.


Bel esempio inventato. Che dire dell'altro tuo commento su un potenziale effetto sull'esecuzione fuori ordine dovuta all'uso di EFLAGS? È puramente teorico o può effettivamente accadere che un JB porti a una pipeline migliore di un JBE?
Rustyx,

@rustyx: l'ho commentato da qualche parte sotto un'altra risposta? I compilatori non emetteranno codice che causa blocchi di flag parziali, e certamente non per una C <o <=. Ma certo, test ecx,ecx/ bt eax, 3/ jbesalterà se è impostato ZF (ecx == 0) o se è impostato CF (bit 3 di EAX == 1), causando un arresto parziale del flag sulla maggior parte delle CPU perché i flag che legge non tutti provengono dall'ultima istruzione per scrivere eventuali flag. Sulla famiglia Sandybridge, in realtà non si blocca, deve solo inserire una fusione in alto. cmp/ testscrive tutte le bandiere, ma btlascia ZF non modificato. felixcloutier.com/x86/bt
Peter Cordes,

2

Solo se le persone che hanno creato i computer sono cattive con la logica booleana. Che non dovrebbero essere.

Ogni confronto ( >= <= > <) può essere eseguito alla stessa velocità.

Qualunque sia il confronto, è solo una sottrazione (la differenza) e vedere se è positivo / negativo.
(Se msbimpostato, il numero è negativo)

Come controllare a >= b? Sottocontrolla a-b >= 0se a-bè positivo.
Come controllare a <= b? Sottocontrolla 0 <= b-ase b-aè positivo.
Come controllare a < b? Sottocontrolla a-b < 0se a-bè negativo.
Come controllare a > b? Sottocontrolla 0 > b-ase b-aè negativo.

In poche parole, il computer può semplicemente farlo sotto il cofano per l'operazione data:

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

e ovviamente il computer non avrebbe effettivamente bisogno di fare l' uno ==0o ==1l'altro.
per il ==0potrebbe essere solo invertire la msbdal circuito.

Comunque, sicuramente non sarebbero a >= bstati calcolati come a>b || a==blol

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.