Qual è il costo in termini di prestazioni di avere un metodo virtuale in una classe C ++?


107

Avere almeno un metodo virtuale in una classe C ++ (o una qualsiasi delle sue classi padre) significa che la classe avrà una tabella virtuale e ogni istanza avrà un puntatore virtuale.

Quindi il costo della memoria è abbastanza chiaro. Il più importante è il costo della memoria sulle istanze (soprattutto se le istanze sono piccole, ad esempio se intendono solo contenere un intero: in questo caso avere un puntatore virtuale in ogni istanza potrebbe raddoppiare la dimensione delle istanze. lo spazio di memoria utilizzato dalle tabelle virtuali, immagino che sia solitamente trascurabile rispetto allo spazio utilizzato dal codice del metodo effettivo.

Questo mi porta alla mia domanda: esiste un costo di prestazione misurabile (cioè impatto sulla velocità) per rendere virtuale un metodo? Ci sarà una ricerca nella tabella virtuale in fase di esecuzione, ad ogni chiamata di metodo, quindi se ci sono chiamate molto frequenti a questo metodo e se questo metodo è molto breve, potrebbe esserci un calo misurabile delle prestazioni? Immagino che dipenda dalla piattaforma, ma qualcuno ha eseguito alcuni benchmark?

Il motivo per cui lo chiedo è che mi sono imbattuto in un bug dovuto al fatto che un programmatore ha dimenticato di definire un metodo virtuale. Non è la prima volta che vedo questo tipo di errore. E ho pensato: perché ci aggiungiamo la parola chiave virtuale, se necessario, invece di rimuovere la parola chiave virtuale quando siamo assolutamente sicuri che sia non è necessaria? Se il costo delle prestazioni è basso, penso che consiglierò semplicemente quanto segue al mio team: rendi semplicemente virtuale ogni metodo per impostazione predefinita, incluso il distruttore, in ogni classe e rimuovilo solo quando necessario. Ti sembra folle?



7
Il confronto tra chiamate virtuali e non virtuali non è affatto banale. Forniscono funzionalità diverse. Se si desidera confrontare le chiamate di funzioni virtuali con l'equivalente C è necessario aggiungere il costo del codice che implementa la funzionalità equivalente della funzione virtuale.
Martin York,

Che è un'istruzione switch o un'istruzione big if. Se fossi intelligente potresti reimplementare usando una tabella di puntatori a funzione ma le probabilità di sbagliare sono molto più alte.
Martin York


7
La domanda riguarda le chiamate di funzione che non devono essere virtuali, quindi il confronto è significativo.
Mark Ransom

Risposte:


104

Ho eseguito alcune temporizzazioni su un processore PowerPC in ordine da 3ghz. In questa architettura, una chiamata di funzione virtuale costa 7 nanosecondi in più di una chiamata di funzione diretta (non virtuale).

Quindi, non vale davvero la pena preoccuparsi del costo a meno che la funzione non sia qualcosa di simile a una banale funzione di accesso Get () / Set (), in cui qualsiasi cosa diversa da inline è una specie di spreco. Un sovraccarico di 7ns su una funzione inline a 0,5ns è grave; un overhead di 7ns su una funzione che impiega 500ms per essere eseguita non ha senso.

Il grande costo delle funzioni virtuali non è in realtà la ricerca di un puntatore a funzione nella tabella v (di solito è solo un singolo ciclo), ma il salto indiretto di solito non può essere previsto dal ramo. Ciò può causare una bolla di pipeline di grandi dimensioni poiché il processore non può recuperare alcuna istruzione fino a quando il salto indiretto (la chiamata tramite il puntatore a funzione) non viene ritirato e viene calcolato un nuovo puntatore di istruzione. Quindi, il costo di una chiamata di funzione virtuale è molto più grande di quanto potrebbe sembrare guardando l'assembly ... ma comunque solo 7 nanosecondi.

Modifica: Andrew, Not Sure, e altri sollevano anche l'ottimo punto che una chiamata di funzione virtuale può causare la mancata visualizzazione della cache delle istruzioni: se salti a un indirizzo di codice che non è nella cache, l'intero programma si ferma mentre il le istruzioni vengono recuperate dalla memoria principale. Questo è sempre uno stallo significativo: su Xenon, circa 650 cicli (dai miei test).

Tuttavia questo non è un problema specifico delle funzioni virtuali perché anche una chiamata diretta a una funzione causerà un errore se si passa a istruzioni che non sono nella cache. Ciò che conta è se la funzione è stata eseguita prima di recente (rendendo più probabile che sia nella cache) e se la tua architettura può prevedere rami statici (non virtuali) e recuperare quelle istruzioni nella cache in anticipo. Il mio PPC no, ma forse l'hardware più recente di Intel sì.

Il mio controllo dei tempi per l'influenza di icache manca sull'esecuzione (deliberatamente, poiché stavo cercando di esaminare la pipeline della CPU in isolamento), quindi scontano quel costo.


3
Il costo in cicli è approssimativamente uguale al numero di fasi della pipeline tra il recupero e la fine del ritiro del ramo. Non è un costo insignificante e può sommarsi, ma a meno che tu non stia cercando di scrivere un loop ad alte prestazioni stretto, probabilmente ci sono pesci perf più grandi da friggere.
Crashworks

7 nano secondi più lunghi di quanto. Se una chiamata normale è di 1 nano secondo che è dignitoso se una chiamata normale è di 70 nano secondi, allora non lo è.
Martin York

Se guardi i tempi, ho scoperto che per una funzione che costa 0,66 ns in linea, l'overhead differenziale di una chiamata diretta di funzione era di 4,8 ns e una funzione virtuale 12,3 ns (rispetto all'inline). È chiaro che se la funzione stessa costa un millisecondo, 7 ns non significa nulla.
Crashworks

2
Più come 600 cicli, ma è un buon punto. L'ho lasciato fuori dai tempi perché ero interessato solo al sovraccarico dovuto alla bolla del gasdotto e al prologo / epilogo. L'icache miss si verifica altrettanto facilmente per una chiamata diretta a una funzione (Xenon non ha un predittore di ramo icache).
Crashworks

2
Dettaglio minore, ma per quanto riguarda "Tuttavia questo non è un problema specifico ..." E 'un po' peggio per la spedizione virtuale come c'è un ulteriore pagina (o due se capita di cadere in un limite di pagina) che deve essere nella cache - per la tabella di invio virtuale della classe.
Tony Delroy

19

C'è sicuramente un sovraccarico misurabile quando si chiama una funzione virtuale: la chiamata deve utilizzare la tabella v per risolvere l'indirizzo della funzione per quel tipo di oggetto. Le istruzioni extra sono l'ultima delle tue preoccupazioni. Non solo vtables previene molte potenziali ottimizzazioni del compilatore (poiché il tipo è polimorfico del compilatore), possono anche distruggere l'I-Cache.

Ovviamente se queste penalità sono significative o meno dipende dall'applicazione, dalla frequenza con cui vengono eseguiti quei percorsi di codice e dai modelli di ereditarietà.

A mio parere, tuttavia, avere tutto come virtuale per impostazione predefinita è una soluzione globale a un problema che potresti risolvere in altri modi.

Forse potresti guardare come sono progettate / documentate / scritte le classi. Generalmente l'intestazione di una classe dovrebbe chiarire quali funzioni possono essere sovrascritte dalle classi derivate e come vengono chiamate. Avere programmatori che scrivono questa documentazione è utile per assicurarsi che siano contrassegnati correttamente come virtuali.

Direi anche che dichiarare ogni funzione come virtuale potrebbe portare a più bug oltre al semplice dimenticare di contrassegnare qualcosa come virtuale. Se tutte le funzioni sono virtuali, tutto può essere sostituito da classi base - pubbliche, protette, private - tutto diventa un gioco leale. Per caso o per intenzione, le sottoclassi potrebbero quindi modificare il comportamento delle funzioni che causano problemi quando vengono utilizzate nell'implementazione di base.


La più grande ottimizzazione persa è in linea, soprattutto se la funzione virtuale è spesso piccola o vuota.
Zan Lynx

@Andrew: interessante punto di vista. Tuttavia, non sono in qualche modo d'accordo con il tuo ultimo paragrafo: se una classe base ha una funzione saveche si basa su un'implementazione specifica di una funzione writenella classe base, allora mi sembra che sia savemal codificata o writedovrebbe essere privata.
MiniQuark

2
Solo perché la scrittura è privata non impedisce che venga sovrascritta. Questo è un altro argomento per non rendere le cose virtuali per impostazione predefinita. In ogni caso stavo pensando al contrario: un'implementazione generica e ben scritta viene sostituita da qualcosa che ha un comportamento specifico e non compatibile.
Andrew Grant,

Votato per il caching: su qualsiasi grande base di codice orientata agli oggetti, se non stai seguendo le pratiche di prestazioni della località del codice, è molto facile per le tue chiamate virtuali causare errori nella cache e causare uno stallo.
Non so il

E uno stallo dell'icache può essere davvero serio: 600 cicli nei miei test.
Crashworks

9

Dipende. :) (Ti aspettavi qualcos'altro?)

Una volta che una classe ottiene una funzione virtuale, non può più essere un tipo di dati POD, (potrebbe non esserlo nemmeno prima, nel qual caso questo non farà la differenza) e questo rende impossibile un'intera gamma di ottimizzazioni.

std :: copy () sui tipi POD semplici può ricorrere a una semplice routine memcpy, ma i tipi non POD devono essere gestiti con maggiore attenzione.

La costruzione diventa molto più lenta perché la vtable deve essere inizializzata. Nel peggiore dei casi, la differenza di prestazioni tra i tipi di dati POD e non POD può essere significativa.

Nel peggiore dei casi, potresti vedere un'esecuzione 5 volte più lenta (quel numero è preso da un progetto universitario che ho fatto di recente per reimplementare alcune classi di libreria standard. Il nostro contenitore ha impiegato circa 5 volte il tempo per costruire non appena il tipo di dati che ha memorizzato ha ottenuto un vtable)

Ovviamente, nella maggior parte dei casi, è improbabile che tu veda differenze di prestazioni misurabili, questo è semplicemente per sottolineare che in alcuni casi limite può essere costoso.

Tuttavia, le prestazioni non dovrebbero essere la tua considerazione principale qui. Rendere tutto virtuale non è una soluzione perfetta per altri motivi.

Consentire a tutto di essere sovrascritto nelle classi derivate rende molto più difficile mantenere invarianti di classe. In che modo una classe garantisce che rimanga in uno stato coerente quando uno qualsiasi dei suoi metodi può essere ridefinito in qualsiasi momento?

Rendere tutto virtuale può eliminare alcuni potenziali bug, ma ne introduce anche di nuovi.


7

Se hai bisogno della funzionalità di invio virtuale, devi pagare il prezzo. Il vantaggio di C ++ è che puoi usare un'implementazione molto efficiente dell'invio virtuale fornito dal compilatore, piuttosto che una versione possibilmente inefficiente che implementerai tu stesso.

Tuttavia, ingombrare te stesso con il sovraccarico se non ne hai bisogno, forse sta andando un po 'troppo lontano. E la maggior parte delle classi non sono progettate per essere ereditate - per creare una buona classe base è necessario qualcosa di più che rendere virtuali le sue funzioni.


Buona risposta ma, IMO, non abbastanza enfatico nella seconda metà: ingombrare te stesso con le spese generali se non ne hai bisogno è, francamente, folle - specialmente quando usi questa lingua il cui mantra è "non pagare per quello che indossi non usare. " Rendere tutto virtuale per impostazione predefinita fino a quando qualcuno non giustifica il motivo per cui può / dovrebbe essere non virtuale è una politica abominevole.
underscore_d

5

L'invio virtuale è un ordine di grandezza più lento di alcune alternative, non tanto a causa dell'indirizzamento quanto per la prevenzione dell'inlining. Di seguito, lo illustro confrontando l'invio virtuale con un'implementazione che incorpora un "numero (identificativo) di tipo" negli oggetti e utilizzando un'istruzione switch per selezionare il codice specifico del tipo. Ciò evita completamente il sovraccarico delle chiamate di funzione, semplicemente facendo un salto locale. Esiste un potenziale costo per la manutenibilità, le dipendenze di ricompilazione ecc. Attraverso la localizzazione forzata (nello switch) della funzionalità specifica del tipo.


IMPLEMENTAZIONE

#include <iostream>
#include <vector>

// virtual dispatch model...

struct Base
{
    virtual int f() const { return 1; }
};

struct Derived : Base
{
    virtual int f() const { return 2; }
};

// alternative: member variable encodes runtime type...

struct Type
{
    Type(int type) : type_(type) { }
    int type_;
};

struct A : Type
{
    A() : Type(1) { }
    int f() const { return 1; }
};

struct B : Type
{
    B() : Type(2) { }
    int f() const { return 2; }
};

struct Timer
{
    Timer() { clock_gettime(CLOCK_MONOTONIC, &from); }
    struct timespec from;
    double elapsed() const
    {
        struct timespec to;
        clock_gettime(CLOCK_MONOTONIC, &to);
        return to.tv_sec - from.tv_sec + 1E-9 * (to.tv_nsec - from.tv_nsec);
    }
};

int main(int argc)
{
  for (int j = 0; j < 3; ++j)
  {
    typedef std::vector<Base*> V;
    V v;

    for (int i = 0; i < 1000; ++i)
        v.push_back(i % 2 ? new Base : (Base*)new Derived);

    int total = 0;

    Timer tv;

    for (int i = 0; i < 100000; ++i)
        for (V::const_iterator i = v.begin(); i != v.end(); ++i)
            total += (*i)->f();

    double tve = tv.elapsed();

    std::cout << "virtual dispatch: " << total << ' ' << tve << '\n';

    // ----------------------------

    typedef std::vector<Type*> W;
    W w;

    for (int i = 0; i < 1000; ++i)
        w.push_back(i % 2 ? (Type*)new A : (Type*)new B);

    total = 0;

    Timer tw;

    for (int i = 0; i < 100000; ++i)
        for (W::const_iterator i = w.begin(); i != w.end(); ++i)
        {
            if ((*i)->type_ == 1)
                total += ((A*)(*i))->f();
            else
                total += ((B*)(*i))->f();
        }

    double twe = tw.elapsed();

    std::cout << "switched: " << total << ' ' << twe << '\n';

    // ----------------------------

    total = 0;

    Timer tw2;

    for (int i = 0; i < 100000; ++i)
        for (W::const_iterator i = w.begin(); i != w.end(); ++i)
            total += (*i)->type_;

    double tw2e = tw2.elapsed();

    std::cout << "overheads: " << total << ' ' << tw2e << '\n';
  }
}

RISULTATI DELLE PRESTAZIONI

Sul mio sistema Linux:

~/dev  g++ -O2 -o vdt vdt.cc -lrt
~/dev  ./vdt                     
virtual dispatch: 150000000 1.28025
switched: 150000000 0.344314
overhead: 150000000 0.229018
virtual dispatch: 150000000 1.285
switched: 150000000 0.345367
overhead: 150000000 0.231051
virtual dispatch: 150000000 1.28969
switched: 150000000 0.345876
overhead: 150000000 0.230726

Ciò suggerisce che un approccio in linea con cambio di numero di tipo è di circa (1,28 - 0,23) / (0,344 - 0,23) = 9,2 volte più veloce. Naturalmente, è specifico per l'esatto sistema testato / flag e versione del compilatore, ecc., Ma generalmente indicativo.


COMMENTI RE VIRTUAL SPATCH

Va detto però che i sovraccarichi delle chiamate di funzioni virtuali sono qualcosa di raramente significativo, e quindi solo per le funzioni banali spesso chiamate (come getter e setter). Anche allora, potresti essere in grado di fornire una singola funzione per ottenere e impostare un sacco di cose contemporaneamente, riducendo al minimo i costi. Le persone si preoccupano troppo della spedizione virtuale, quindi fai il profilo prima di trovare alternative scomode. Il problema principale con loro è che eseguono una chiamata di funzione fuori linea, sebbene delocalizzino anche il codice eseguito che cambia i modelli di utilizzo della cache (in meglio o (più spesso) in peggio).


Ho posto una domanda riguardo al tuo codice perché ho dei risultati "strani" usando g++/ clange -lrt. Ho pensato che valesse la pena menzionarlo qui per i futuri lettori.
Holt

@Holt: bella domanda visti i risultati sconcertanti! Darò un'occhiata più da vicino nei pochi giorni se avrò mezza possibilità. Saluti.
Tony Delroy

3

Il costo aggiuntivo è praticamente nulla nella maggior parte degli scenari. (scusate il gioco di parole). ejac ha già pubblicato misure relative sensibili.

La cosa più importante a cui rinunciare sono le possibili ottimizzazioni a causa dell'inlining. Possono essere particolarmente utili se la funzione viene chiamata con parametri costanti. Questo raramente fa una vera differenza, ma in alcuni casi può essere enorme.


Per quanto riguarda le ottimizzazioni:
è importante conoscere e considerare il costo relativo dei costrutti della tua lingua. La notazione Big O è solo metà della storia: come scala la tua applicazione . L'altra metà è il fattore costante di fronte ad essa.

Come regola generale, non farei di tutto per evitare le funzioni virtuali, a meno che non ci siano indicazioni chiare e specifiche che si tratta di un collo di bottiglia. Un design pulito viene sempre prima, ma è solo uno degli stakeholder che non dovrebbe danneggiare indebitamente gli altri.


Esempio artificioso: un distruttore virtuale vuoto su un array di un milione di piccoli elementi può far passare almeno 4 MB di dati, distruggendo la cache. Se quel distruttore può essere inline via, i dati non verranno toccati.

Quando si scrive il codice della libreria, tali considerazioni sono tutt'altro che premature. Non sai mai quanti loop verranno inseriti attorno alla tua funzione.


2

Mentre tutti gli altri hanno ragione riguardo alle prestazioni dei metodi virtuali e simili, penso che il vero problema sia se il team conosce la definizione della parola chiave virtuale in C ++.

Considera questo codice, qual è l'output?

#include <stdio.h>

class A
{
public:
    void Foo()
    {
        printf("A::Foo()\n");
    }
};

class B : public A
{
public:
    void Foo()
    {
        printf("B::Foo()\n");
    }
};

int main(int argc, char** argv)
{    
    A* a = new A();
    a->Foo();

    B* b = new B();
    b->Foo();

    A* a2 = new B();
    a2->Foo();

    return 0;
}

Niente di sorprendente qui:

A::Foo()
B::Foo()
A::Foo()

Poiché niente è virtuale. Se la parola chiave virtuale viene aggiunta all'inizio di Foo in entrambe le classi A e B, otteniamo questo per l'output:

A::Foo()
B::Foo()
B::Foo()

Più o meno quello che tutti si aspettano.

Ora, hai detto che ci sono bug perché qualcuno ha dimenticato di aggiungere una parola chiave virtuale. Quindi considera questo codice (dove la parola chiave virtuale viene aggiunta alla classe A, ma non B). Qual è quindi l'output?

#include <stdio.h>

class A
{
public:
    virtual void Foo()
    {
        printf("A::Foo()\n");
    }
};

class B : public A
{
public:
    void Foo()
    {
        printf("B::Foo()\n");
    }
};

int main(int argc, char** argv)
{    
    A* a = new A();
    a->Foo();

    B* b = new B();
    b->Foo();

    A* a2 = new B();
    a2->Foo();

    return 0;
}

Risposta: lo stesso come se la parola chiave virtuale fosse aggiunta a B? Il motivo è che la firma di B :: Foo corrisponde esattamente a A :: Foo () e poiché Foo di A è virtuale, lo è anche di B.

Consideriamo ora il caso in cui Foo di B è virtuale e A non lo è. Qual è quindi l'output? In questo caso, l'output è

A::Foo()
B::Foo()
A::Foo()

La parola chiave virtuale funziona verso il basso nella gerarchia, non verso l'alto. Non rende mai virtuali i metodi della classe base. La prima volta che si incontra un metodo virtuale nella gerarchia è quando inizia il polimorfismo. Non c'è un modo per le classi successive di fare in modo che le classi precedenti abbiano metodi virtuali.

Non dimenticare che i metodi virtuali significano che questa classe darà alle classi future la possibilità di sovrascrivere / modificare alcuni dei suoi comportamenti.

Quindi, se hai una regola per rimuovere la parola chiave virtuale, potrebbe non avere l'effetto desiderato.

La parola chiave virtuale in C ++ è un concetto potente. Dovresti assicurarti che ogni membro del team conosca davvero questo concetto in modo che possa essere utilizzato come progettato.


Ciao Tommy, grazie per il tutorial. Il bug che avevamo era dovuto a una parola chiave "virtuale" mancante in un metodo della classe base. A proposito, sto dicendo di rendere virtuali tutte le funzioni (non il contrario), quindi, quando chiaramente non è necessario, rimuovere la parola chiave "virtuale".
MiniQuark

@MiniQuark: Tommy Hui sta dicendo che se rendi tutte le funzioni virtuali, un programmatore potrebbe finire per rimuovere la parola chiave in una classe derivata, senza rendersi conto che non ha alcun effetto. Avresti bisogno di un modo per assicurarti che la rimozione della parola chiave virtuale avvenga sempre nella classe di base.
M. Dudley

1

A seconda della piattaforma, il sovraccarico di una chiamata virtuale può essere molto indesiderabile. Dichiarando ogni funzione virtuale, essenzialmente le stai chiamando tutte tramite un puntatore a funzione. Per lo meno questa è una dereferenziazione extra, ma su alcune piattaforme PPC utilizzerà istruzioni microcodificate o altrimenti lente per ottenere ciò.

Raccomanderei contro il tuo suggerimento per questo motivo, ma se ti aiuta a prevenire i bug, potrebbe valere la pena fare un compromesso. Non posso fare a meno di pensare che deve esserci una via di mezzo che vale la pena trovare, però.


-1

Richiederà solo un paio di istruzioni asm extra per chiamare il metodo virtuale.

Ma non credo che ti preoccupi che fun (int a, int b) abbia un paio di istruzioni extra 'push' rispetto a fun (). Quindi non preoccuparti anche dei virtuali, finché non ti trovi in ​​una situazione speciale e vedi che porta davvero ai problemi.

PS Se hai un metodo virtuale, assicurati di avere un distruttore virtuale. In questo modo eviterai possibili problemi


In risposta ai commenti "xtofl" e "Tom". Ho fatto piccoli test con 3 funzioni:

  1. Virtuale
  2. Normale
  3. Normale con 3 parametri int

Il mio test è stato una semplice iterazione:

for(int it = 0; it < 100000000; it ++) {
    test.Method();
}

E qui i risultati:

  1. 3.913 sec
  2. 3.873 sec
  3. 3.970 sec

È stato compilato da VC ++ in modalità debug. Ho eseguito solo 5 test per metodo e ho calcolato il valore medio (quindi i risultati potrebbero essere piuttosto imprecisi) ... In ogni caso, i valori sono quasi uguali assumendo 100 milioni di chiamate. E il metodo con 3 push / pop extra è stato più lento.

Il punto principale è che se non ti piace l'analogia con il push / pop, pensi a extra if / else nel tuo codice? Pensi alla pipeline della CPU quando aggiungi extra if / else ;-) Inoltre, non sai mai su quale CPU verrà eseguito il codice ... Il solito compilatore può generare codice più ottimale per una CPU e meno ottimale per un'altra ( Intel Compilatore C ++ )


2
l'ASM aggiuntivo potrebbe semplicemente attivare un errore di pagina (che non sarebbe lì per funzioni non virtuali) - Penso che tu semplifichi enormemente il problema.
xtofl

2
+1 al commento di xtofl. Le funzioni virtuali introducono il riferimento indiretto, che introduce "bolle" nella pipeline e influisce sul comportamento della memorizzazione nella cache.
Tom

1
Il tempismo di qualsiasi cosa in modalità debug non ha senso. MSVC rende il codice molto lento in modalità di debug e il sovraccarico del ciclo probabilmente nasconde la maggior parte della differenza. Se stai mirando a prestazioni elevate, sì, dovresti pensare di ridurre al minimo i rami if / else nel percorso veloce. Vedere agner.org/optimize per ulteriori informazioni sull'ottimizzazione delle prestazioni x86 di basso livello. (Anche alcuni altri collegamenti nel wiki dei tag x86
Peter Cordes

1
@ Tom: il punto chiave qui è che le funzioni non virtuali possono inline, ma il virtuale no (a meno che il compilatore non possa devirtualizzare, ad esempio se hai usato finalnel tuo override e hai un puntatore al tipo derivato, piuttosto che al tipo base ). Questo test chiamava ogni volta la stessa funzione virtuale, quindi prevedeva perfettamente; nessuna bolla nella pipeline ad eccezione del callthroughput limitato . E quello indiretto callpotrebbe essere un altro paio di uops. La previsione dei rami funziona bene anche per i rami indiretti, soprattutto se si trovano sempre nella stessa destinazione.
Peter Cordes

Questo cade nella trappola comune dei microbenchmark: sembra veloce quando i predittori di ramo sono caldi e nient'altro sta succedendo. L'overhead di previsione errata è maggiore per l'indiretto callche per il diretto call. (E sì, anche le normali callistruzioni richiedono una previsione. La fase di recupero deve conoscere l'indirizzo successivo da recuperare prima che questo blocco venga decodificato, quindi deve prevedere il successivo blocco di recupero in base all'indirizzo del blocco corrente, piuttosto che all'indirizzo dell'istruzione. come prevedere dove in questo blocco c'è un'istruzione di ramo ...)
Peter Cordes
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.