"IF" è costoso?


98

Non posso, per la vita di me, ricordare cosa ha detto esattamente il nostro insegnante quel giorno e spero che probabilmente lo sapresti.

Il modulo è "Data Structures and Algorithms" e ci ha detto qualcosa sulla falsariga di:

L' ifaffermazione è il [qualcosa] più costoso. [qualcosa] registra [qualcosa].

Sì, ho una memoria orribile e mi dispiace davvero molto, ma ho cercato su Google per ore e non è uscito nulla. Qualche idea?


29
Chiedere al tuo insegnante un'opzione?
Michael Myers

7
Perché non invii un'email al tuo insegnante? È improbabile che qualcuno su SO sappia cosa ha detto il tuo insegnante, a meno che non fossero lì in quel momento (o il tuo insegnante stesso leggesse SO).
Bill Karwin,

11
E ovviamente un link alla risposta
bobobobo

Le istruzioni If o in particolare le espressioni "?:" Nei linguaggi con parentesi graffe influenzate dal C possono essere implementate da speciali istruzioni di esecuzione condizionale, ad esempio sui processori x86 e arm. Queste sono istruzioni che eseguono o non eseguono alcune operazioni basate su un test precedente. L'uso di queste eccellenti istruzioni evita del tutto la necessità di istruzioni di salto condizionale / diramazione / "goto". Un enorme miglioramento delle prestazioni in alcune situazioni, rendendo il flusso del programma completamente prevedibile poiché procede dritto senza (possibilmente imprevedibile) saltare in diversi punti del codice.
Cecil Ward

Un buon compilatore a volte potrebbe aver bisogno di una spinta nella giusta direzione in modo che utilizzi istruzioni condizionali invece di essere stupido e usare salti condizionali, riorganizzando il codice e possibilmente usando un'intelligente aritmetica in un'espressione o un? : espressione. Non giocare con questo a meno che tu non conosca veramente il tuo asm e abbia letto ad esempio le guide all'ottimizzazione di Agner Fog. I compilatori a volte lo fanno bene indipendentemente dal fatto che le dichiarazioni if ​​o? : vengono utilizzate le espressioni.
Cecil Ward

Risposte:


185

Al livello più basso (nell'hardware), sì, se sono costosi. Per capire perché, devi capire come funzionano le pipeline .

L'istruzione corrente da eseguire è memorizzata in qualcosa di solito chiamato puntatore dell'istruzione (IP) o contatore di programma (PC); questi termini sono sinonimi, ma termini diversi vengono utilizzati con architetture diverse. Per la maggior parte delle istruzioni, il PC dell'istruzione successiva è solo il PC corrente più la lunghezza dell'istruzione corrente. Per la maggior parte delle architetture RISC, le istruzioni sono tutte di lunghezza costante, quindi il PC può essere incrementato di una quantità costante. Per le architetture CISC come x86, le istruzioni possono essere di lunghezza variabile, quindi la logica che decodifica l'istruzione deve calcolare quanto tempo è l'istruzione corrente per trovare la posizione dell'istruzione successiva.

Per le istruzioni di ramo , tuttavia, l'istruzione successiva da eseguire non è la posizione successiva dopo l'istruzione corrente. I rami sono gotos: dicono al processore dove si trova l'istruzione successiva. I rami possono essere condizionali o incondizionati e la posizione di destinazione può essere fissa o calcolata.

Condizionale vs. incondizionato è facile da capire: un ramo condizionale viene preso solo se una certa condizione è valida (ad esempio se un numero è uguale a un altro); se il ramo non viene preso, il controllo procede all'istruzione successiva dopo il ramo come di consueto. Per i rami incondizionati, il ramo viene sempre preso. I rami condizionali vengono visualizzati nelle ifistruzioni e nei test di controllo di fore whilecicli. I rami incondizionati si presentano in cicli infiniti, chiamate di funzioni, ritorni di funzioni breake continueistruzioni, la famigerata gotoistruzione e molti altri (questi elenchi sono tutt'altro che esaustivi).

Il target del ramo è un'altra questione importante. La maggior parte delle filiali ha una destinazione di diramazione fissa: vanno in una posizione specifica nel codice che viene fissata in fase di compilazione. Ciò include ifistruzioni, cicli di tutti i tipi, chiamate di funzioni regolari e molti altri. I rami calcolati calcolano la destinazione del ramo in fase di esecuzione. Ciò include switchistruzioni (a volte), ritorno da una funzione, chiamate di funzioni virtuali e chiamate di puntatori a funzione.

Quindi cosa significa tutto questo per le prestazioni? Quando il processore vede apparire un'istruzione di branch nella sua pipeline, deve capire come continuare a riempire la sua pipeline. Per capire quali istruzioni vengono dopo il ramo nel flusso del programma, è necessario sapere due cose: (1) se il ramo verrà preso e (2) l'obiettivo del ramo. Capirlo è chiamato previsione del ramo ed è un problema impegnativo. Se il processore indovina correttamente, il programma continua a piena velocità. Se invece il processore indovina in modo errato , ha semplicemente passato un po 'di tempo a calcolare la cosa sbagliata. Ora deve svuotare la sua pipeline e ricaricarla con le istruzioni dal percorso di esecuzione corretto. Conclusione: un grande successo in termini di prestazioni.

Pertanto, il motivo per cui le dichiarazioni if ​​sono costose è dovuto a previsioni errate di filiale . Questo è solo al livello più basso. Se stai scrivendo codice di alto livello, non devi preoccuparti affatto di questi dettagli. Dovresti preoccuparti di questo solo se stai scrivendo codice estremamente critico per le prestazioni in C o in assembly. In questo caso, scrivere codice senza rami può essere spesso superiore al codice che si dirama, anche se sono necessarie molte altre istruzioni. Ci sono alcuni trucchetti bit-giocherellando che potete fare per calcolare cose come abs(), min()e max()senza di ramificazione.


20
Non sono solo errori di previsione del ramo. I rami inibiscono anche il riordino delle istruzioni, a livello di compilatore e, in una certa misura, anche a livello di CPU (per una CPU fuori servizio, ovviamente). Bella risposta dettagliata però.
jalf

5
Se i linguaggi di alto livello vengono infine tradotti in linguaggi di basso livello e stai scrivendo codice molto incentrato sulle prestazioni, non ottieni ancora nulla scrivendo codice che eviti le istruzioni if? Questo concetto non si applica ai linguaggi di livello superiore?
c ..

18

"Costoso" è un termine molto relativo, soprattutto in relazione a un'istruzione " if" poiché devi anche tenere in considerazione il costo della condizione. Ciò potrebbe variare da poche brevi istruzioni della CPU al test del risultato di una funzione che richiama un database remoto.

Non me ne preoccuperei. A meno che tu non stia facendo la programmazione incorporata, probabilmente non dovresti preoccuparti del costo di " if". Per la maggior parte dei programmatori, semplicemente non sarà mai il fattore trainante per le prestazioni della tua app.


1
Sicuramente relativo ... cmp / cond jmp è ancora più veloce di un mul su molti processori.
Brian Knoblauch

4
Sì, sono d'accordo che non dovrei preoccuparmene. Non sto cercando di ottimizzare nulla qui. Sto solo cercando di scoprire e imparare. ;)
pek

15

I rami, in particolare sui microprocessori con architettura RISC, sono alcune delle istruzioni più costose. Questo perché su molte architetture, il compilatore predice quale percorso di esecuzione verrà preso più probabilmente e inserisce quelle istruzioni successivamente nell'eseguibile, quindi saranno già nella cache della CPU quando si verifica il ramo. Se il ramo va nella direzione opposta, deve tornare alla memoria principale e recuperare le nuove istruzioni: è abbastanza costoso. Su molte architetture RISC, tutte le istruzioni sono un ciclo eccetto branch (che spesso è di 2 cicli). Non stiamo parlando di un costo importante qui, quindi non preoccuparti. Inoltre, il compilatore ottimizzerà meglio di te il 99% delle volte: ) Una delle cose davvero fantastiche dell'architettura EPIC (Itanium è un esempio) è che memorizza nella cache (e inizia l'elaborazione) le istruzioni da entrambi i lati del ramo, quindi scarta il set di cui non ha bisogno una volta che il risultato del ramo è conosciuto. Ciò consente di risparmiare l'accesso alla memoria extra di un'architettura tipica nel caso in cui si diramasse lungo il percorso imprevisto.


13

Consulta l'articolo Prestazioni migliori grazie all'eliminazione dei rami sulle prestazioni delle celle. Un altro divertente è questo post sulle selezioni senza diramazioni sul blog sul rilevamento delle collisioni in tempo reale.

Oltre alle ottime risposte già pubblicate in risposta a questa domanda, vorrei ricordare che sebbene le istruzioni "if" siano considerate costose operazioni di basso livello, cercando di utilizzare tecniche di programmazione senza rami in un ambiente di livello superiore , come un linguaggio di scripting o un livello di logica aziendale (indipendentemente dalla lingua), potrebbero essere ridicolmente inappropriati.

La stragrande maggioranza delle volte, i programmi dovrebbero essere scritti prima per chiarezza e poi ottimizzati per le prestazioni. Esistono numerosi domini problematici in cui le prestazioni sono fondamentali, ma il semplice fatto è che la maggior parte degli sviluppatori non sta scrivendo moduli da utilizzare in profondità nel nucleo di un motore di rendering o una simulazione di dinamica dei fluidi ad alte prestazioni che viene eseguita per settimane e settimane. Quando la massima priorità è che la tua soluzione "funzioni e basta", l'ultima cosa a cui pensare dovrebbe essere se puoi o meno risparmiare sull'overhead di un'istruzione condizionale nel tuo codice.


Infatti! Si potrebbe anche aggiungere che, quando si codifica in un linguaggio che incoraggia le chiamate (fondamentalmente, qualsiasi cosa diversa da assembler o C senza stdlib), l'interferenza della pipeline dalle normali tecniche di programmazione supererà qualsiasi domanda sulla ramificazione condizionale.
Ross Patterson

10

ifdi per sé non è lento. La lentezza è sempre relativa, scommetto per la mia vita che non hai mai sentito il "sovraccarico" di un'affermazione if. Se hai intenzione di creare un codice ad alte prestazioni, potresti comunque voler evitare i rami. Ciò che rende iflento è che il processore sta precaricando il codice da dopo ifbasato su alcune euristiche e quant'altro. Impedirà inoltre alle pipeline di eseguire il codice direttamente dopo l' ifistruzione branch nel codice macchina, poiché il processore non sa ancora quale percorso verrà preso (in un processore pipeline, più istruzioni vengono intercalate ed eseguite). Il codice eseguito potrebbe dover essere eseguito al contrario (se l'altro ramo è stato preso. Si chiama branch misprediction), oppure deve noopessere compilato in quei punti in modo che ciò non avvenga.

Se ifè male, allora switchè il male troppo, e &&, ||anche. Non ti preoccupare.


7

Al livello più basso possibile if(dopo aver calcolato tutti i prerequisiti specifici dell'app per particolare if):

  • alcune istruzioni di prova
  • salta in qualche punto del codice se il test ha esito positivo, altrimenti procedi in avanti.

Costi associati a ciò:

  • un confronto di basso livello - di solito 1 operazione cpu, super economico
  • potenziale salto, che può essere costoso

Risuona perché i salti sono costosi:

  • puoi saltare al codice arbirario che risiede ovunque nella memoria, se si scopre che non è memorizzato nella cache dalla cpu - abbiamo un problema, perché abbiamo bisogno di accedere alla memoria principale, che è più lenta
  • le moderne CPU eseguono la predizione dei branch. Cercano di indovinare se avrà successo o meno ed eseguono il codice in anticipo nella pipeline, quindi velocizza le cose. Se la previsione fallisce, tutti i calcoli effettuati in anticipo dalla pipeline devono essere invalidati. Anche questa è un'operazione costosa

Quindi per riassumere:

  • Se può essere espansivo, se davvero, davvero, ti interessa davvero le prestazioni.
  • Dovresti preoccuparti se e solo se stai scrivendo raytracer in tempo reale o simulazione biologica o qualcosa di simile. Non c'è motivo di preoccuparsene nella maggior parte del mondo reale.

Porta questo al livello successivo: che dire delle istruzioni if ​​annidate e / o composte? La spesa può diventare abbastanza evidente rapidamente se qualcuno scrive molte dichiarazioni if ​​come questa. E poiché alla maggior parte degli sviluppatori le istruzioni if ​​sembrano un'operazione così fondamentale, evitare la ramificazione condizionale contorta è spesso relegato a una preoccupazione stilistica. Le preoccupazioni stilistiche sono ancora importanti, ma spesso nella foga del momento possono essere la prima preoccupazione da ignorare.
jaydel

7

I processori moderni hanno pipeline di esecuzione lunghe, il che significa che diverse istruzioni vengono eseguite in varie fasi contemporaneamente. Potrebbero non sempre conoscere il risultato di un'istruzione quando inizia a essere eseguita quella successiva. Quando si imbattono in un salto condizionale (se) a volte devono aspettare che la pipeline sia vuota prima di poter sapere in che direzione deve andare il puntatore dell'istruzione.

Lo considero un lungo treno merci. Può trasportare molto carico velocemente in linea retta, ma curva male.

Il Pentium 4 (Prescott) aveva una pipeline notoriamente lunga di 31 stadi.

Più su Wikipedia


3
+1 per la metafora del treno merci - Lo ricorderò per la prossima volta che avrò bisogno di spiegare le pipeline del processore.
Daniel Pryden

6

Forse il branching uccide il precaricamento delle istruzioni della CPU?


Dopo la mia ... "ricerca" ho imparato a conoscere le tabelle di salto e le ramificazioni per le istruzioni switch, ma niente sulle istruzioni if. Potresti approfondire un po 'questo?
pek

IIRC, la CPU di solito esegue il precaricamento delle istruzioni lungo un unico probabile percorso di esecuzione, ma un'istruzione "if" che causa un salto dal percorso di esecuzione previsto invaliderà le istruzioni precaricate e il pretech dovrà essere riavviato.
activout.se

Qualsiasi processore decente dovrebbe avere capacità di previsione dei rami che cercheranno di indovinare se un ramo verrà preso o meno e le istruzioni di precaricamento basate sulla previsione (che è generalmente abbastanza buona). GCC ha anche estensioni C che consentono a un programmatore di fornire suggerimenti per i predittori di ramo.
mipadi

2
Inoltre, la CPU di solito guarda avanti per iniziare a eseguire in anticipo le istruzioni imminenti (non solo precaricarle), e il compilatore prova a riordinare le istruzioni, e questo diventa pericoloso tra i rami, quindi puoi davvero uccidere la pianificazione delle istruzioni con troppi rami. Il che fa male alle prestazioni.
jalf

6

Si noti inoltre che all'interno di un ciclo non lo è necessariamente molto costoso.

La CPU moderna assume alla prima visita di un'istruzione if, che "if-body" debba essere preso (o detto in un altro modo: assume anche un loop-body da prendere più volte) (*). Alla seconda e successiva visita, (la CPU) può forse esaminare la tabella della cronologia del ramo e vedere com'era la condizione l'ultima volta (era vero? Era falso?). Se l'ultima volta era falso, l'esecuzione speculativa procederà all '"altro" dell'if o oltre il ciclo.

(*) La regola è in realtà " ramo in avanti non preso, ramo indietro preso ". In un'istruzione if, c'è solo un salto [in avanti] (al punto dopo il corpo if) se la condizione è falsa (ricorda: la CPU comunque assume di non prendere un salto / salto), ma in un ciclo , c'è forse un ramo in avanti alla posizione dopo il ciclo (da non prendere) e un ramo all'indietro dopo la ripetizione (da prendere).

Questo è anche uno dei motivi per cui una chiamata a una funzione virtuale o una chiamata a un puntatore a funzione non è così peggiore come molti pensano ( http://phresnel.org/blog/ )


5

Come sottolineato da molti, i rami condizionali possono essere molto lenti su un computer moderno.

Detto questo, ci sono un sacco di rami condizionali che non vivono nelle istruzioni if, non puoi sempre dire cosa verrà in mente il compilatore e preoccuparsi di quanto tempo impiegheranno le istruzioni di base è praticamente sempre la cosa sbagliata fare. (Se puoi dire cosa genererà il compilatore in modo affidabile, potresti non avere un buon compilatore ottimizzato.)


4

L'unica cosa a cui posso immaginare questo potrebbe riferirsi è il fatto che ifun'affermazione generalmente può risultare in un ramo. A seconda delle specifiche dell'architettura del processore, i rami possono causare blocchi della pipeline o altre situazioni non ottimali.

Tuttavia, questo è estremamente specifico per la situazione: la maggior parte dei processori moderni dispone di funzionalità di previsione dei rami che tentano di ridurre al minimo gli effetti negativi della ramificazione. Un altro esempio potrebbe essere il modo in cui l'architettura ARM (e probabilmente altre) può gestire la logica condizionale - ARM ha un'esecuzione condizionale a livello di istruzione, quindi la logica condizionale semplice non produce ramificazioni - le istruzioni vengono eseguite semplicemente come NOP se le condizioni non sono soddisfatte.

Detto questo, ottieni la tua logica corretta prima di preoccuparti di queste cose. Il codice errato non è ottimizzato come puoi ottenere.


Ho sentito che le istruzioni condizionali di ARM inibiscono ILP, quindi potrebbero semplicemente aggirare il problema.
JD

3

Le CPU sono profondamente pipeline. Qualsiasi istruzione branch (if / for / while / switch / etc) significa che la CPU non sa veramente quale istruzione caricare ed eseguire successivamente.

La CPU si blocca in attesa di sapere cosa fare o la CPU fa un'ipotesi. Nel caso di una CPU più vecchia, o se l'ipotesi è sbagliata, dovrai subire uno stallo della pipeline mentre va a caricare l'istruzione corretta. A seconda della CPU, questo può arrivare fino a 10-20 istruzioni di stallo.

Le CPU moderne cercano di evitarlo eseguendo una buona previsione dei rami ed eseguendo più percorsi contemporaneamente e mantenendo solo quello effettivo. Questo aiuta molto, ma può solo andare così lontano.

Buona fortuna in classe.

Inoltre, se devi preoccuparti di questo nella vita reale, probabilmente stai progettando il sistema operativo, la grafica in tempo reale, il calcolo scientifico o qualcosa di simile alla CPU. Profilo prima di preoccuparsi.


2

Scrivi i tuoi programmi nel modo più chiaro, semplice e pulito che non sia ovviamente inefficiente. Questo fa il miglior uso della risorsa più costosa, tu. Che si tratti di scrivere o di eseguire il debug successivo (richiede comprensione) del programma. Se le prestazioni non sono sufficienti, misuradove si trovano i colli di bottiglia e vedere come mitigarli. Solo in occasioni estremamente rare dovrai preoccuparti delle istruzioni individuali (fonte) quando lo fai. Le prestazioni riguardano la selezione degli algoritmi e delle strutture dati giusti nella prima riga, un'attenta programmazione, l'ottenimento di una macchina abbastanza veloce. Usa un buon compilatore, rimarrai sorpreso nel vedere il tipo di codice che ristruttura un compilatore moderno. La ristrutturazione del codice per le prestazioni è una sorta di ultima risorsa, il codice diventa più complesso (quindi più buggier), più difficile da modificare e quindi più costoso.



0

Una volta ho avuto questa discussione con un mio amico. Stava usando un algoritmo del cerchio molto ingenuo, ma sosteneva che fosse più veloce del mio (il tipo che calcola solo 1/8 del cerchio) perché il mio usava if. Alla fine, l'istruzione if è stata sostituita con sqrt e in qualche modo è stato più veloce. Forse perché la FPU ha sqrt integrato?


-1

Il più costoso in termini di utilizzo di ALU? Utilizza i registri della CPU per memorizzare i valori da confrontare e impiega tempo per recuperare e confrontare i valori ogni volta che viene eseguita l'istruzione if.

Pertanto, un'ottimizzazione di questo è fare un confronto e memorizzare il risultato come una variabile prima che il ciclo venga eseguito.

Sto solo cercando di interpretare le tue parole mancanti.

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.