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 if
istruzioni e nei test di controllo di for
e while
cicli. I rami incondizionati si presentano in cicli infiniti, chiamate di funzioni, ritorni di funzioni break
e continue
istruzioni, la famigerata goto
istruzione 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 if
istruzioni, 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 switch
istruzioni (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.