Quale codice è meglio per l'ottimizzazione della previsione delle filiali?


10

Data la previsione del ramo e anche l'effetto delle ottimizzazioni del compilatore, quale codice tende a offrire prestazioni superiori?

Si noti che bRareExceptionPresent rappresenta una condizione non comune. Non è il normale percorso della logica.

/* MOST COMMON path must branch around IF clause */

bool SomeFunction(bool bRareExceptionPresent)
{
  // abort before function
  if(bRareExceptionPresent)
  {
     return false;
  }    
  .. function primary body ..    
  return true;
}

/* MOST COMMON path does NOT branch */

bool SomeFunction(bool bRareExceptionPresent)
{
  if(!bRareExceptionPresent)
  {
    .. function primary body ..
  }
  else
  {
    return false;
  }
  return true;
}

9
Ho intenzione di uscire su un arto qui e dire che non c'è alcuna differenza.
Robert Harvey,

7
Questo probabilmente dipende dalla CPU specifica per la quale stai compilando, poiché hanno architetture di pipelining diverse (slot di ritardo vs slot di ritardo). Il tempo che hai trascorso a pensarci è probabilmente molto più del tempo risparmiato durante l'esecuzione: prima il profilo, quindi l'ottimizzazione.

2
È quasi certamente una micro-ottimizzazione prematura.
Robert Harvey,

2
@MichaelT Sì, la profilazione è davvero l'unico modo affidabile per sapere cosa sta realmente succedendo con le prestazioni del codice sul target, sulla piattaforma, nel suo contesto. Tuttavia, ero curioso di sapere se uno era generalmente preferito.
dyasta,

1
@RobertHarvey: è una micro-ottimizzazione prematura, tranne nei casi in cui entrambe le condizioni sono soddisfatte: (1) il ciclo è chiamato miliardi (non milioni) di volte; e (2) ironicamente, quando il corpo del loop è minuscolo in termini di codice macchina. Condizione n. 2 significa che la frazione di tempo speso in spese generali non è insignificante rispetto al tempo speso in lavori utili. La buona notizia è che di solito, in tali situazioni in cui entrambe le condizioni sono soddisfatte, il SIMD (vettorializzazione), che è per sua natura senza rami, risolverà tutti i problemi di prestazione.
rwong

Risposte:


10

Nel mondo di oggi, non importa molto, se non del tutto.

La previsione dinamica dei rami (qualcosa pensata per decenni (vedi Analisi dei piani di previsione dei rami dinamici sui carichi di lavoro del sistema pubblicati nel 1996) sono abbastanza comuni.

Un esempio di questo può essere trovato nel processore ARM. Dal Centro informazioni sul braccio su Branch Prediction

Per migliorare la precisione della previsione del ramo, viene utilizzata una combinazione di tecniche statiche e dinamiche.

La domanda allora è "che cos'è la predizione del ramo dinamico nel processore arm?" La lettura confinata della predizione del ramo dinamico mostra che utilizza uno schema di predizione a 2 bit (descritto nel documento) crea informazioni sul fatto che il ramo sia preso in modo forte o debole o non preso.

Nel tempo (e per tempo intendo alcuni passaggi attraverso quel blocco) questo crea informazioni su come andrà il codice.

Per la previsione statica , osserva l'aspetto del codice stesso e il modo in cui il ramo viene eseguito sul test - a un'istruzione precedente o a un'altra nel codice:

Lo schema utilizzato nel processore ARM1136JF-S prevede che tutti i rami condizionali in avanti non vengano presi e tutti i rami all'indietro. Circa il 65% di tutti i rami è preceduto da un numero sufficiente di cicli non derivati ​​da essere completamente previsto.

Come accennato da Sparky, questo si basa sulla comprensione che i loop più spesso, loop. Il ciclo si dirama all'indietro (ha un ramo alla fine del ciclo per riavviarlo in alto) - normalmente lo fa.

Il pericolo di provare a indovinare il compilatore è che non sai come sarà effettivamente compilato (e ottimizzato) quel codice. E per la maggior parte, non importa. Con la previsione dinamica, due volte attraverso la funzione prevede un salto sull'istruzione guard per un ritorno prematuro. Se le prestazioni di due condutture scaricate hanno prestazioni critiche, ci sono altre cose di cui preoccuparsi.

Il tempo necessario per leggere uno stile rispetto all'altro è probabilmente di maggiore importanza: rendere il codice pulito in modo che un essere umano possa leggerlo, perché il compilatore andrà benissimo, non importa quanto disordinato o idealizzato si scriva il codice.


7
Una famosa domanda StackOverflow ha mostrato che la predizione dei salti non importa, anche oggi.
Florian Margaine,

3
@FlorianMargaine mentre è importante, trovarsi in una situazione in cui conta davvero sembra richiedere la comprensione di ciò che si sta compilando e di come funziona (arm vs x86 vs mips ...). Scrivere codice cercando di eseguire questa micro-ottimizzazione all'inizio probabilmente funziona da premesse errate e non ottiene l'effetto desiderato.

Bene, ovviamente, non citiamo DK. Ma penso che questa domanda fosse chiaramente nel senso dell'ottimizzazione, quando hai già superato la fase di profilazione. :-)
Florian Margaine,

2
@MichaelT Bella risposta, e sono molto d'accordo con la tua conclusione. Questo tipo di pre-profilazione / ottimizzazione astratta può sicuramente essere controproducente. Finisce per essere un gioco d'ipotesi, che induce a prendere decisioni di progettazione per motivi irrazionali. Tuttavia, mi sono trovato curioso; o
dyasta il


9

La mia comprensione è che la prima volta che la CPU incontra un ramo, predirà (se supportato) che i rami in avanti non vengono presi e quelli al contrario. La logica di ciò è che si presume che vengano fatti dei loop (che in genere si ramificano all'indietro).

Su alcuni processori, è possibile dare un suggerimento nelle istruzioni di assemblaggio su quale percorso è più probabile. I dettagli di questo mi sfuggono al momento.

Inoltre, alcuni compilatori C supportano anche la previsione dei rami statici in modo da poter dire al compilatore quale ramo è più probabile. A sua volta, potrebbe riorganizzare il codice generato o utilizzare le istruzioni modificate per sfruttare queste informazioni (o anche semplicemente ignorarle).

__builtin_expect((long)!!(x), 1L)  /* GNU C to indicate that <x> will likely be TRUE */
__builtin_expect((long)!!(x), 0L)  /* GNU C to indicate that <x> will likely be FALSE */

Spero che sia di aiuto.


3
"La mia comprensione è che la prima volta che la CPU incontra un ramo, predirà (se supportato) che i rami in avanti non vengono presi e quelli al contrario". Questo è un pensiero molto interessante. Hai qualche prova che questo sia effettivamente implementato in architetture comuni?
Blubb,

5
Direttamente dalla bocca del cavallo: per impostazione predefinita un ramo in avanti non viene preso. L'impostazione predefinita di un ramo all'indietro è presa . E dalla stessa pagina: "prefisso 0x3E - prevedere staticamente un ramo come preso".
Salterio

Esiste un pragma agnostico della piattaforma che è equivoco a __builtin_expect?
MarcusJ,
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.