C'è un suggerimento per il compilatore per GCC di forzare la predizione dei rami ad andare sempre in un certo modo?


118

Per le architetture Intel, esiste un modo per istruire il compilatore GCC a generare codice che imponga sempre la previsione dei rami in un modo particolare nel mio codice? L'hardware Intel supporta anche questo? E gli altri compilatori o hardware?

Lo userei nel codice C ++ dove conosco il caso in cui desidero correre velocemente e non mi interessa il rallentamento quando è necessario prendere l'altro ramo anche quando ha recentemente preso quel ramo.

for (;;) {
  if (normal) { // How to tell compiler to always branch predict true value?
    doSomethingNormal();
  } else {
    exceptionalCase();
  }
}

Come domanda successiva per Evdzhan Mustafa, il suggerimento può semplicemente specificare un suggerimento per la prima volta che il processore incontra l'istruzione, tutte le successive predizione dei rami, funzionando normalmente?


potrebbe anche lanciare un'eccezione se qualcosa diventa anormale (che è indipendente dal compilatore)
Shep

Risposte:


9

A partire da C ++ 20, gli attributi probabile e improbabile dovrebbero essere standardizzati e sono già supportati in g ++ 9 . Quindi, come discusso qui , puoi scrivere

if (a>b) {
  /* code you expect to run often */
  [[likely]] /* last statement */
}

ad esempio nel codice seguente il blocco else viene inline grazie al [[unlikely]]blocco if

int oftendone( int a, int b );
int rarelydone( int a, int b );
int finaltrafo( int );

int divides( int number, int prime ) {
  int almostreturnvalue;
  if ( ( number % prime ) == 0 ) {
    auto k                         = rarelydone( number, prime );
    auto l                         = rarelydone( number, k );
    [[unlikely]] almostreturnvalue = rarelydone( k, l );
  } else {
    auto a            = oftendone( number, prime );
    almostreturnvalue = oftendone( a, a );
  }
  return finaltrafo( almostreturnvalue );
}

collegamento godbolt che confronta la presenza / assenza dell'attributo


Perché usare [[unlikely]]in ifvs [[likely]]in else?
WilliamKF

nessun motivo, sono appena finito in questa costellazione dopo aver provato dove deve andare l'attributo.
pseyfert

Abbastanza bello. Peccato che il metodo non sia applicabile alle versioni C ++ precedenti.
Maxim Egorushkin il

Fantastico collegamento godbolt
Lewis Kelsey

87

GCC supporta la funzione __builtin_expect(long exp, long c)per fornire questo tipo di funzionalità. Puoi controllare la documentazione qui .

Dov'è expla condizione utilizzata ed cè il valore atteso. Ad esempio nel tuo caso vorresti

if (__builtin_expect(normal, 1))

A causa della sintassi scomoda, questo viene solitamente utilizzato definendo due macro personalizzate come

#define likely(x)    __builtin_expect (!!(x), 1)
#define unlikely(x)  __builtin_expect (!!(x), 0)

solo per facilitare il compito.

Ricorda che:

  1. questo non è standard
  2. un predittore di ramo compilatore / cpu è probabilmente più abile di te nel decidere cose del genere quindi questa potrebbe essere una microottimizzazione prematura

3
C'è un motivo per cui mostri una macro e non una constexprfunzione?
Colombo

22
@Columbo: non credo che una constexprfunzione possa sostituire questa macro. Deve essere ifdirettamente nella dichiarazione, credo. La stessa ragione assertnon potrebbe mai essere una constexprfunzione.
Mooing Duck

1
@MooingDuck Sono d'accordo, anche se ci sono più motivi per affermare .
Shafik Yaghmour

7
@Columbo un motivo per usare una macro sarebbe perché questo è uno dei pochi posti in C o C ++ in cui una macro è semanticamente più corretta di una funzione. La funzione sembra funzionare solo a causa dell'ottimizzazione ( è un'ottimizzazione: constexprparla solo della semantica del valore, non dell'inlining dell'assembly specifico dell'implementazione); la semplice interpretazione (no inline) del codice è priva di significato. Non c'è alcun motivo per utilizzare una funzione per questo.
Leushenko

2
@Leushenko Considera che di per __builtin_expectsé è un suggerimento per l'ottimizzazione, quindi sostenere che un metodo che semplifica il suo utilizzo dipende dall'ottimizzazione non è ... convincente. Inoltre, non ho aggiunto lo constexprspecificatore per farlo funzionare in primo luogo, ma per farlo funzionare in espressioni costanti. E sì, ci sono ragioni per usare una funzione. Ad esempio, non vorrei inquinare il mio intero spazio dei nomi con un nome carino come likely. Dovrei usare ad esempio LIKELY, per sottolineare che è una macro ed evitare collisioni, ma è semplicemente brutto.
Columbo

46

gcc ha long __builtin_expect (long exp, long c) ( enfasi mia ):

È possibile utilizzare __builtin_expect per fornire al compilatore le informazioni sulla previsione dei rami. In generale, dovresti preferire utilizzare il feedback del profilo effettivo per questo (-fprofile-arcs), poiché i programmatori sono notoriamente pessimi nel prevedere come funzionano effettivamente i loro programmi . Tuttavia, ci sono applicazioni in cui questi dati sono difficili da raccogliere.

Il valore restituito è il valore di exp, che dovrebbe essere un'espressione integrale. La semantica dell'integrato è che ci si aspetta che exp == c. Per esempio:

if (__builtin_expect (x, 0))
   foo ();

indica che non ci aspettiamo di chiamare foo, poiché ci aspettiamo che x sia zero. Poiché sei limitato alle espressioni integrali per exp, dovresti usare costruzioni come

if (__builtin_expect (ptr != NULL, 1))
   foo (*ptr);

durante il test di puntatori o valori a virgola mobile.

Come osserva la documentazione, dovresti preferire utilizzare il feedback del profilo effettivo e questo articolo mostra un esempio pratico di questo e come nel loro caso almeno finisce per essere un miglioramento rispetto all'uso __builtin_expect. Vedi anche Come utilizzare le ottimizzazioni guidate dal profilo in g ++? .

Possiamo anche trovare un articolo per i neofiti del kernel Linux sulle macro kernal probabili () e improbabili () che utilizzano questa caratteristica:

#define likely(x)       __builtin_expect(!!(x), 1)
#define unlikely(x)     __builtin_expect(!!(x), 0)

Nota l' !!usato nella macro, possiamo trovare la spiegazione in Perché usare !! (condizione) invece di (condizione)? .

Solo perché questa tecnica è usata nel kernel Linux non significa che abbia sempre senso usarla. Possiamo vedere da questa domanda che ho recentemente risposto alla differenza tra le prestazioni della funzione quando si passa il parametro come costante di tempo di compilazione o variabile che molte tecniche di ottimizzazione manuale non funzionano nel caso generale. Abbiamo bisogno di profilare il codice con attenzione per capire se una tecnica è efficace. Molte vecchie tecniche potrebbero non essere nemmeno rilevanti con le moderne ottimizzazioni del compilatore.

Nota, sebbene i builtin non siano portabili, clang supporta anche __builtin_expect .

Anche su alcune architetture potrebbe non fare la differenza .


Ciò che è abbastanza buono per il kernel Linux non è sufficiente per C ++ 11.
Maxim Egorushkin

@ MaximEgorushkin nota, in realtà non ne consiglio l'uso, infatti la documentazione gcc che cito che è la mia prima citazione non usa nemmeno quella tecnica. Direi che lo scopo principale della mia risposta è considerare attentamente le alternative prima di intraprendere questa strada.
Shafik Yaghmour

44

No non c'è. (Almeno sui moderni processori x86.)

__builtin_expectmenzionato in altre risposte influenza il modo in cui gcc organizza il codice assembly. Non influenza direttamente il predittore di ramo della CPU. Naturalmente, ci saranno effetti indiretti sulla previsione dei rami causati dal riordino del codice. Ma sui moderni processori x86 non ci sono istruzioni che dica alla CPU "presumere che questo ramo sia / non sia preso".

Vedere questa domanda per maggiori dettagli: Intel x86 0x2E / 0x3E Prefix Branch Prediction effettivamente utilizzato?

Per essere chiari, __builtin_expecte / o l'uso di -fprofile-arcs può migliorare le prestazioni del tuo codice, sia dando suggerimenti al predittore di ramo attraverso il layout del codice (vedi Ottimizzazione delle prestazioni dell'assembly x86-64 - Allineamento e previsione dei rami ), sia migliorando anche il comportamento della cache mantenendo il codice "improbabile" lontano dal codice "probabile".


9
Questo non è corretto. In tutte le versioni moderne di x86, l'algoritmo di predizione predefinito consiste nel prevedere che i rami in avanti non vengono presi e che i rami all'indietro lo sono (vedere software.intel.com/en-us/articles/… ). Quindi, riorganizzando il codice si può effettivamente dare un suggerimento per la CPU. Questo è esattamente ciò che fa GCC quando usi __builtin_expect.
Nemo

6
@Nemo, hai letto oltre la prima frase della mia risposta? Tutto quello che hai detto è coperto dalla mia risposta o nei link forniti. La domanda chiedeva se fosse possibile "forzare la predizione del ramo ad andare sempre in un certo modo", a cui la risposta è "no" e non ho sentito che altre risposte fossero abbastanza chiare al riguardo.
Artelius

4
OK, avrei dovuto leggere più attentamente. Mi sembra che questa risposta sia tecnicamente corretta, ma inutile, poiché l'interrogante sta ovviamente cercando __builtin_expect. Quindi questo dovrebbe essere solo un commento. Ma non è falso, quindi ho rimosso il mio voto negativo.
Nemo

IMO non è inutile; è un utile chiarimento su come funzionano effettivamente CPU e compilatori, che potrebbe essere rilevante per l'analisi delle prestazioni con / senza queste opzioni. ad esempio, di solito non è possibile utilizzare __builtin_expectper creare banalmente un caso di test che è possibile misurare con perf statche avrà un tasso di errori di previsione molto elevato. Influisce solo sul layout del ramo . E a proposito, Intel da Sandybridge o almeno Haswell lo fa non usa molto / affatto la previsione statica; c'è sempre qualche previsione nel BHT, che si tratti di un alias non aggiornato o meno. xania.org/201602/bpu-part-two
Peter Cordes

24

Il modo corretto per definire le macro probabili / improbabili in C ++ 11 è il seguente:

#define LIKELY(condition) __builtin_expect(static_cast<bool>(condition), 1)
#define UNLIKELY(condition) __builtin_expect(static_cast<bool>(condition), 0)

Questo metodo è compatibile con tutte le versioni C ++, a differenza [[likely]], ma si basa su un'estensione non standard __builtin_expect.


Quando queste macro vengono definite in questo modo:

#define LIKELY(condition) __builtin_expect(!!(condition), 1)

Ciò potrebbe cambiare il significato delle ifdichiarazioni e rompere il codice. Considera il codice seguente:

#include <iostream>

struct A
{
    explicit operator bool() const { return true; }
    operator int() const { return 0; }
};

#define LIKELY(condition) __builtin_expect((condition), 1)

int main() {
    A a;
    if(a)
        std::cout << "if(a) is true\n";
    if(LIKELY(a))
        std::cout << "if(LIKELY(a)) is true\n";
    else
        std::cout << "if(LIKELY(a)) is false\n";
}

E la sua uscita:

if(a) is true
if(LIKELY(a)) is false

Come puoi vedere, la definizione di LIKELY using !!as a cast boolrompe la semantica di if.

Il punto qui non è quello operator int()e operator bool()dovrebbe essere correlato. Che è una buona pratica.

Piuttosto che usare !!(x)invece di static_cast<bool>(x)perdere il contesto per le conversioni contestuali C ++ 11 .


Si noti che le conversioni contestuali sono arrivate tramite un difetto nel 2012 e anche alla fine del 2014 c'era ancora divergenza nell'implementazione. In realtà sembra che il case a cui ho collegato non funzioni ancora per gcc.
Shafik Yaghmour

@ShafikYaghmour Questa è un'osservazione interessante per quanto riguarda la conversione contestuale coinvolta switch, grazie. La conversione contestuale coinvolta qui è parte integrante del tipo boole dei cinque contesti specifici elencati lì , che non includono il switchcontesto.
Maxim Egorushkin

Questo riguarda solo C ++, giusto? Quindi non c'è motivo di cambiare i progetti C esistenti da usare (_Bool)(condition), perché C non ha l'overloading degli operatori.
Peter Cordes

2
Nel tuo esempio, hai usato solo (condition), no !!(condition). Entrambi sono truedopo averlo modificato (testato con g ++ 7.1). Puoi costruire un esempio che dimostri effettivamente il problema di cui parli quando usi !!booleanize?
Peter Cordes

3
Come ha sottolineato Peter Cordes, dici "Quando queste macro [sono] definite in questo modo:" e poi mostri una macro usando '!!', "potresti cambiare il significato delle istruzioni if ​​e rompere il codice. Considera il codice seguente:" ... e poi mostri il codice che non usa "!!" affatto - che è noto per essere rotto anche prima di C ++ 11. Si prega di cambiare la risposta per mostrare un esempio in cui la macro data (usando !!) va storta.
Carlo Wood

18

Come le altre risposte hanno tutte adeguatamente suggerito, puoi usare __builtin_expectper dare al compilatore un suggerimento su come organizzare il codice dell'assembly. Come sottolineano i documenti ufficiali , nella maggior parte dei casi, l'assemblatore integrato nel tuo cervello non sarà buono come quello creato dal team di GCC. È sempre meglio utilizzare i dati del profilo effettivo per ottimizzare il codice, piuttosto che indovinare.

Lungo linee simili, ma non ancora menzionate, è un modo specifico di GCC per forzare il compilatore a generare codice su un percorso "freddo". Ciò implica l'uso degli attributi noinlinee cold, che fanno esattamente quello che suonano come fanno. Questi attributi possono essere applicati solo alle funzioni, ma con C ++ 11 è possibile dichiarare funzioni lambda inline e questi due attributi possono essere applicati anche alle funzioni lambda.

Anche se questo rientra ancora nella categoria generale di una microottimizzazione, e quindi si applica il consiglio standard - test non indovinare - penso che sia più generalmente utile di __builtin_expect. Quasi nessuna generazione del processore x86 utilizza i suggerimenti di previsione dei rami ( riferimento ), quindi l'unica cosa che sarai in grado di influenzare comunque è l'ordine del codice assembly. Dato che sai cos'è il codice di gestione degli errori o "caso limite", puoi usare questa annotazione per assicurarti che il compilatore non preveda mai un ramo ad esso e lo colleghi lontano dal codice "caldo" durante l'ottimizzazione per le dimensioni.

Utilizzo del campione:

void FooTheBar(void* pFoo)
{
    if (pFoo == nullptr)
    {
        // Oh no! A null pointer is an error, but maybe this is a public-facing
        // function, so we have to be prepared for anything. Yet, we don't want
        // the error-handling code to fill up the instruction cache, so we will
        // force it out-of-line and onto a "cold" path.
        [&]() __attribute__((noinline,cold)) {
            HandleError(...);
        }();
    }

    // Do normal stuff
    
}

Ancora meglio, GCC lo ignorerà automaticamente a favore del feedback del profilo quando è disponibile (ad esempio, durante la compilazione con -fprofile-use).

Consulta la documentazione ufficiale qui: https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#Common-Function-Attributes


2
I prefissi dei suggerimenti per la previsione del ramo vengono ignorati perché non sono necessari; puoi ottenere lo stesso identico effetto semplicemente riordinando il tuo codice. (L'algoritmo di predizione dei rami predefinito consiste nell'indovinare che i rami all'indietro vengono presi e quelli in avanti no.) Quindi puoi, in effetti, dare un suggerimento alla CPU, e questo è ciò che __builtin_expectfa. Non è affatto inutile. Hai ragione che l' coldattributo è anche utile, ma tu sottovaluti l'utilità di __builtin_expectcredo.
Nemo

Le moderne CPU Intel non utilizzano la previsione dei rami statici. L'algoritmo che descrivi, @Nemo, in cui i rami all'indietro sono previsti come presi e quelli in avanti sono previsti come non presi è stato utilizzato nei processori precedenti, e fino al Pentium M o giù di lì, ma i progetti moderni fondamentalmente indovinano casualmente, indicizzando nel loro ramo tabelle in cui si aspetterebbe di trovare informazioni su quel ramo e utilizzando qualsiasi informazione presente (anche se può essere essenzialmente spazzatura). Quindi i suggerimenti per la previsione dei rami sarebbero teoricamente utili, ma forse non nella pratica, motivo per cui Intel li ha rimossi.
Cody Grey

Per essere chiari, l'implementazione della predizione dei rami è estremamente complicata e i vincoli di spazio nei commenti mi hanno costretto a semplificare notevolmente. Questa sarebbe davvero una risposta completa in sé e per sé. Potrebbero esserci ancora tracce di predizione dei rami statici nelle moderne microarchitetture, come Haswell, ma non è così semplice come una volta.
Cody Grey

Hai un riferimento per "le moderne CPU Intel non usano la previsione del ramo statico"? L'articolo di Intel ( software.intel.com/en-us/articles/… ) dice il contrario ... Ma questo è del 2011
Nemo

Non ho davvero un riferimento ufficiale, @Nemo. Intel è estremamente attenta agli algoritmi di previsione dei rami utilizzati nei suoi chip, trattandoli come segreti commerciali. La maggior parte di ciò che si conosce è stato calcolato mediante test empirici. Come sempre, i materiali di Agner Fog sono le migliori risorse, ma anche lui dice: "Il predittore di rami sembra essere stato ridisegnato nell'Haswell, ma si sa molto poco della sua costruzione". Non ricordo dove ho visto per la prima volta i benchmark che dimostrano che la BP statica non è stata più utilizzata, sfortunatamente.
Cody Grey

5

__builtin_expect può essere usato per dire al compilatore in che direzione ti aspetti che vada un ramo. Questo può influenzare il modo in cui viene generato il codice. I processori tipici eseguono il codice più velocemente in sequenza. Quindi se scrivi

if (__builtin_expect (x == 0, 0)) ++count;
if (__builtin_expect (y == 0, 0)) ++count;
if (__builtin_expect (z == 0, 0)) ++count;

il compilatore genererà codice come

if (x == 0) goto if1;
back1: if (y == 0) goto if2;
back2: if (z == 0) goto if3;
back3: ;
...
if1: ++count; goto back1;
if2: ++count; goto back2;
if3: ++count; goto back3;

Se il tuo suggerimento è corretto, questo eseguirà il codice senza alcun ramo effettivamente eseguito. Verrà eseguito più velocemente della sequenza normale, in cui ogni istruzione if si ramificherà attorno al codice condizionale ed eseguirà tre rami.

I processori x86 più recenti hanno istruzioni per i rami che dovrebbero essere presi, o per i rami che dovrebbero non essere presi (c'è un prefisso di istruzione; non sono sicuro dei dettagli). Non sono sicuro che il processore lo usi. Non è molto utile, perché la previsione dei rami lo gestirà perfettamente. Quindi non penso che tu possa effettivamente influenzare la previsione del ramo .


2

Per quanto riguarda l'OP, no, non c'è modo in GCC di dire al processore di presumere sempre che il ramo sia o non sia preso. Quello che hai è __builtin_expect, che fa quello che dicono gli altri. Inoltre, penso che tu non voglia dire al processore se il ramo è preso o meno sempre . I processori odierni, come l'architettura Intel, possono riconoscere modelli abbastanza complessi e adattarsi in modo efficace.

Tuttavia, ci sono volte in cui vuoi assumere il controllo se per impostazione predefinita un ramo è previsto o meno: quando sai il codice verrà chiamato "freddo" rispetto alle statistiche di ramificazione.

Un esempio concreto: il codice di gestione delle eccezioni. Per definizione il codice di gestione avverrà eccezionalmente, ma forse quando si verifica si desidera il massimo delle prestazioni (potrebbe esserci un errore critico da togliere il prima possibile), quindi potresti voler controllare la previsione predefinita.

Un altro esempio: puoi classificare il tuo input e saltare nel codice che gestisce il risultato della tua classificazione. Se ci sono molte classificazioni, il processore può raccogliere statistiche ma perderle perché la stessa classificazione non avviene abbastanza presto e le risorse di previsione sono dedicate al codice chiamato di recente. Vorrei che ci fosse una primitiva per dire al processore "per favore non dedicare risorse di previsione a questo codice" nel modo in cui a volte puoi dire "non memorizzare nella cache".

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.