Le eccezioni in C ++ sono molto lente

Stavo osservando Systematic Error Handling in C ++ – Andrei Alexandrescu afferma che Exceptions in C++ sono molto lente.

Voglio sapere che questo è ancora vero per C++98

Il modello principale utilizzato oggi per le eccezioni (Itanium ABI, VC ++ 64 bit) è l’eccezione del modello Zero-Cost.

L’idea è che invece di perdere tempo impostando una guardia e verificando esplicitamente la presenza di eccezioni ovunque, il compilatore genera una tabella laterale che mappa qualsiasi punto che può generare un’eccezione (Contatore di programma) in un elenco di gestori. Quando viene lanciata un’eccezione, questa lista viene consultata per scegliere il gestore di destra (se presente) e lo stack viene svolto.

Rispetto alla tipica strategia if (error) :

  • il modello Zero-Cost, come suggerisce il nome, è gratuito quando non si verificano eccezioni
  • costa circa 10x / 20x un if si verifica un’eccezione

Il costo, tuttavia, non è banale da misurare:

  • Il side-table è generalmente freddo , e quindi recuperarlo dalla memoria richiede molto tempo
  • Determinare il right handler coinvolge RTTI: molti descrittori RTTI da recuperare, sparsi per la memoria e operazioni complesse da eseguire (fondamentalmente un test dynamic_cast per ogni gestore)

Quindi, per lo più mancano le cache, e quindi non banale rispetto al puro codice CPU.

Nota: per maggiori dettagli, leggere il rapporto TR18015, capitolo 5.4 Gestione delle eccezioni (pdf)

Quindi, sì, le eccezioni sono lente sul percorso eccezionale , ma sono altrimenti più veloci dei controlli espliciti ( if strategia) in generale.

Nota: secondo NoSenseEtAl, Andrei Alexandrescu sembra mettere in discussione questo “più veloce”. Ho misurato personalmente un’accelerazione nei miei programmi e devo ancora vedere una dimostrazione della perdita di ottimizzazione.


Importa ?

Direi che non è così. Un programma dovrebbe essere scritto tenendo a mente la leggibilità , non le prestazioni (almeno, non come primo criterio). Le eccezioni devono essere utilizzate quando ci si aspetta che il chiamante non possa o non voglia maneggiare l’errore sul posto e farlo passare in pila. Bonus: in C ++ 11 è ansible eseguire il marshalling delle eccezioni tra thread utilizzando la libreria standard.

Questo però è sottile, sostengo che map::find non dovrebbe buttare ma sto bene con map::find restituisce un checked_ptr che getta se un tentativo di dereferenziamento fallisce perché è nullo: in quest’ultimo caso, come nel caso di la class introdotta da Alexandrescu, il chiamante sceglie tra controllo esplicito e affidamento su eccezioni. Responsabilizzare il chiamante senza dargli più responsabilità è di solito un segno di buon design.

Quando la domanda è stata postata, ero in viaggio verso il dottore, con un taxi in attesa, quindi ho avuto solo il tempo per un breve commento. Ma avendo ora commentato e upvoted e downvoted farei meglio ad aggiungere la mia risposta. Anche se la risposta di Matthieu è già abbastanza buona.


Le eccezioni sono particolarmente lente in C ++, rispetto ad altre lingue?

Re il reclamo

“Stavo osservando Systematic Error Handling in C ++ – Andrei Alexandrescu afferma che le eccezioni in C ++ sono molto lente.”

Se questo è letteralmente ciò che sostiene Andrei, allora per una volta è molto fuorviante, se non addirittura sbagliato. Per le eccezioni sollevate / generate è sempre lento rispetto ad altre operazioni di base nella lingua, indipendentemente dal linguaggio di programmazione . Non solo in C ++ o più in C ++ che in altre lingue, come indica la presunta affermazione.

In generale, principalmente a prescindere dalla lingua, le due caratteristiche linguistiche di base che sono ordini di grandezza più lente del resto, perché traducono in chiamate di routine che gestiscono strutture dati complesse, sono

  • lancio di eccezione, e

  • allocazione dynamic della memoria.

Fortunatamente in C ++ si può spesso evitare entrambi in un codice time-critical.

Sfortunatamente non esiste una cosa del genere come un pranzo gratis , anche se l’efficienza predefinita del C ++ si avvicina molto. 🙂 Per l’efficienza ottenuta evitando il lancio di eccezioni e l’allocazione dynamic della memoria è generalmente ottenuta mediante la codifica ad un livello più basso di astrazione, usando C ++ come una “C migliore”. E l’astrazione inferiore significa maggiore “complessità”.

Maggiore complessità significa più tempo dedicato alla manutenzione e poco o nessun beneficio dal riutilizzo del codice, che sono reali costi monetari, anche se difficili da stimare o misurare. Cioè, con C ++ si può, se lo si desidera, scambiare l’efficienza di un programmatore per l’efficienza di esecuzione. Se farlo è in gran parte una decisione ingegneristica e sensitiva, perché in pratica solo il guadagno, non il costo, può essere facilmente stimato e misurato.


Esistono misure oggettive di prestazioni di lancio delle eccezioni del C ++?

Sì, il comitato internazionale di standardizzazione del C ++ ha pubblicato un rapporto tecnico sulle prestazioni del C ++, TR18015 .


Cosa significa che le eccezioni sono “lente”?

Principalmente significa che un throw può richiedere un tempo molto lungo rispetto ad un incarico int , a causa della ricerca di un gestore.

Come TR18015 discute nella sua sezione 5.4 “Eccezioni” ci sono due principali strategie di implementazione della gestione delle eccezioni,

  • l’approccio in cui ogni blocco di try imposta in modo dinamico l’intercettazione di eccezioni, in modo che venga eseguita una ricerca sulla catena dynamic di gestori quando viene generata un’eccezione e

  • l’approccio in cui il compilatore genera tabelle di ricerca statiche che vengono utilizzate per determinare il gestore di un’eccezione generata.

Il primo approccio molto flessibile e generale è quasi forzato in Windows a 32 bit, mentre in terra a 64 bit e in * nix-land viene comunemente utilizzato il secondo approccio molto più efficiente.

Inoltre, come discusso da quel rapporto, per ogni approccio ci sono tre aree principali in cui le manipolazioni delle eccezioni hanno un impatto sull’efficienza:

  • try blocchi,

  • funzioni regolari (opportunità di ottimizzazione), e

  • throw espressioni.

Principalmente, con l’approccio del gestore dinamico (Windows a 32 bit) la gestione delle eccezioni ha un impatto sui blocchi di try , principalmente a prescindere dalla lingua (perché è forzata dallo schema di gestione delle eccezioni strutturate di Windows), mentre l’approccio alla tabella statica ha circa zero costi per try blocchi. Discutere di ciò richiederebbe molto più spazio e ricerca di quanto sia pratico per una risposta SO. Quindi, vedi il rapporto per i dettagli.

Sfortunatamente il rapporto, del 2006, è già un po ‘datato a partire dalla fine del 2012 e, per quanto ne so, non c’è nulla di paragonabile a ciò che è più recente.

Un’altra prospettiva importante è che l’impatto dell’uso delle eccezioni sulla performance è molto diverso dall’efficienza isolata delle funzionalità linguistiche di supporto, perché, come osserva la relazione,

“Quando si considera la gestione delle eccezioni, deve essere confrontato con metodi alternativi di gestione degli errori”.

Per esempio:

  • Costi di manutenzione dovuti a diversi stili di programmazione (correttezza)

  • Sito di chiamata ridondante in if controllo degli errori rispetto al try centralizzato

  • Problemi di memorizzazione nella cache (ad esempio, il codice più breve può rientrare nella cache)

Il report ha un elenco diverso di aspetti da considerare, ma in ogni caso l’unico modo pratico per ottenere informazioni sull’efficienza di esecuzione è probabilmente quello di implementare lo stesso programma usando l’eccezione e non usando eccezioni, entro un limite deciso in fase di sviluppo, e con gli sviluppatori familiarità con ogni modo, e quindi MISURA .


Qual è un buon modo per evitare il sovraccarico delle eccezioni?

La correttezza quasi sempre supera l’efficienza.

Senza eccezioni, ciò che segue può facilmente accadere:

  1. Qualche codice P è pensato per ottenere una risorsa o calcolare alcune informazioni.

  2. Il codice chiamante C dovrebbe aver verificato l’esito positivo / negativo, ma non lo ha fatto.

  3. Una risorsa inesistente o un’informazione non valida viene utilizzata nel codice che segue C, causando un caos generale.

Il problema principale è il punto (2), dove con il solito schema di codice di ritorno il codice chiamante C non è obbligato a controllare.

Esistono due approcci principali che obbligano tale controllo:

  • Dove P lancia direttamente un’eccezione quando fallisce.

  • Dove P restituisce un object che C deve esaminare prima di utilizzare il suo valore principale (altrimenti un’eccezione o una terminazione).

Il secondo approccio fu AFAIK, descritto per la prima volta da Barton e Nackman nel loro libro * Scientific and Engineering C ++: Un’introduzione con tecniche ed esempi avanzati , in cui introdusse una class chiamata Fallow per un risultato di funzione “ansible”. Una class simile chiamata optional è ora offerta dalla libreria Boost. E puoi facilmente implementare una class Optional tu stesso, usando un std::vector come std::vector valore per il caso del risultato non POD.

Con il primo approccio, il codice chiamante C non ha altra scelta che utilizzare le tecniche di gestione delle eccezioni. Con il secondo approccio, tuttavia, il codice chiamante C può decidere autonomamente se eseguire il controllo basato o la gestione generale delle eccezioni. Pertanto, il secondo approccio supporta il bilanciamento tra programmatore e efficienza temporale.


Qual è l’impatto dei vari standard C ++, sulle prestazioni eccezionali?

“Voglio sapere è ancora così per C ++ 98”

C ++ 98 era il primo standard C ++. Per le eccezioni ha introdotto una gerarchia standard di classi di eccezioni (purtroppo piuttosto imperfette). Il principale impatto sulle prestazioni è stata la possibilità di specifiche delle eccezioni (rimosse in C ++ 11), che tuttavia non sono mai state completamente implementate dal compilatore C ++ di Windows principale Visual C ++: Visual C ++ accetta la syntax delle eccezioni C ++ 98, ma ignora semplicemente specifiche di eccezione.

C ++ 03 era solo una rettifica tecnica di C ++ 98. L’unico veramente nuovo in C ++ 03 era l’ inizializzazione del valore . Che non ha nulla a che fare con le eccezioni.

Con lo standard C ++ 11 sono state rimosse le specifiche generali di eccezione e sostituite con la parola chiave noexcept .

Lo standard C ++ 11 ha inoltre aggiunto il supporto per l’archiviazione e la rilocalizzazione delle eccezioni, il che è ottimo per propagare eccezioni C ++ attraverso callback in linguaggio C. Questo supporto limita in modo efficace il modo in cui è ansible memorizzare l’eccezione corrente. Tuttavia, per quanto ne so, questo non ha alcun impatto sulle prestazioni, se non nella misura in cui la gestione delle eccezioni del codice più recente può essere più facilmente utilizzata su entrambi i lati di un callback in linguaggio C.

Dipende dal compilatore.

Il GCC, ad esempio, era noto per le sue scarse prestazioni nella gestione delle eccezioni, ma negli ultimi anni è notevolmente migliorato.

Si noti tuttavia che la gestione delle eccezioni dovrebbe – come dice il nome – essere l’eccezione piuttosto che la regola nella progettazione del software. Quando si dispone di un’applicazione che genera così tante eccezioni al secondo che incide sulle prestazioni e questa operazione è ancora considerata normale, allora si dovrebbe pensare a fare le cose in modo diverso.

Le eccezioni sono un ottimo modo per rendere il codice più leggibile ottenendo tutto quel codice di gestione degli errori goffo, ma non appena diventano parte del normale stream del programma, diventano davvero difficili da seguire. Ricorda che un throw è praticamente un gioco di ruolo sotto mentite spoglie.

Sì, ma non importa. Perché?
Leggi questo:
https://blogs.msdn.com/b/ericlippert/archive/2008/09/10/vexing-exceptions.aspx

Sostanzialmente si dice che l’utilizzo di eccezioni come descritto da Alexandrescu (rallentamento 50x perché usano catch come else ) è semplicemente sbagliato. Detto questo per ppl a chi piace farlo piace C ++ 22 🙂 aggiungerebbe qualcosa come:
(si noti che questo dovrebbe essere il linguaggio di base dal momento che è fondamentalmente il compilatore che genera il codice da uno esistente)

 result = attempt>("12345"); //lexical_cast is boost function, 'attempt' //... is the language construct that pretty much generates function from lexical_cast, generated function is the same as the original one except that fact that throws are replaced by return(and exception type that was in place of the return is placed in a result, but NO exception is thrown)... //... By default std::exception is replaced, ofc precise configuration is possible if (result) { int x = result.get(); // or result.result; } else { // even possible to see what is the exception that would have happened in original function switch (result.exception_type()) //... } 

PS nota anche che anche se le eccezioni sono così lente … non è un problema se non passi molto tempo in quella parte del codice durante l’esecuzione … Per esempio se la divisione float è lenta e la fai 4x più veloce che importa se passi lo 0,3% del tuo tempo facendo la divisione FP …

Come in silico ha detto che dipende dall’implementazione, ma in generale le eccezioni sono considerate lente per qualsiasi implementazione e non dovrebbero essere utilizzate nel codice ad alte prestazioni.

EDIT: Non sto dicendo di non usarli affatto, ma per il codice di rendimento elevato è meglio evitarli.

Le eccezioni non dovrebbero accadere. Se ti aspetti che succedano, lo stai facendo nel modo sbagliato.

Questo è il motivo per cui lo standard non imposta i dettagli di implementazione (inoltre, dipende dal sistema operativo in genere, in Win32 viene chiamata RaiseException). Perché se succede, non dovrebbe preoccuparsi di quanto sia lento.