Stavo guardando Systematic Error Handling in C ++ - Andrei Alexandrescu afferma che le eccezioni in C ++ sono molto lente.
È ancora vero per C ++ 98?
Stavo guardando Systematic Error Handling in C ++ - Andrei Alexandrescu afferma che le eccezioni in C ++ sono molto lente.
È ancora vero per C ++ 98?
Risposte:
Il modello principale utilizzato oggi per le eccezioni (Itanium ABI, VC ++ 64 bit) sono le eccezioni del modello a costo zero.
L'idea è che invece di perdere tempo impostando una guardia e controllando esplicitamente la presenza di eccezioni ovunque, il compilatore generi una tabella laterale che mappa qualsiasi punto che può lanciare un'eccezione (contatore del programma) a un elenco di gestori. Quando viene generata un'eccezione, questo elenco viene consultato per scegliere il gestore destro (se presente) e lo stack viene svolto.
Rispetto alla if (error)strategia tipica :
ifquando si verifica un'eccezioneIl costo però non è banale da misurare:
dynamic_casttest per ogni gestore)Quindi, per lo più la cache manca, e quindi non è banale rispetto al puro codice della CPU.
Nota: per maggiori dettagli, leggere il report 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 ( ifstrategia) in generale.
Nota: Andrei Alexandrescu sembra mettere in dubbio questo "più veloce". Personalmente ho visto le cose oscillare in entrambi i modi, alcuni programmi sono più veloci con le eccezioni e altri sono più veloci con i rami, quindi sembra esserci una perdita di ottimizzazione in determinate condizioni.
Importa ?
Direi di no. Un programma dovrebbe essere scritto pensando alla leggibilità , non alle prestazioni (almeno, non come primo criterio). Le eccezioni devono essere utilizzate quando ci si aspetta che il chiamante non possa o non vorrà gestire il fallimento sul posto e passarlo allo stack. Bonus: in C ++ 11 è possibile eseguire il marshalling delle eccezioni tra i thread utilizzando la libreria standard.
Questo è sottile però, sostengo che map::findnon dovrebbe lanciare ma mi va bene map::findrestituire un checked_ptrche lancia se un tentativo di dereferenziare fallisce perché è nullo: in quest'ultimo caso, come nel caso della classe introdotta da Alexandrescu, il chiamante sceglie tra controllo esplicito e fare affidamento sulle eccezioni. Dare potere al chiamante senza dargli maggiori responsabilità di solito è un segno di buon design.
abortti permetterà di misurare l'impronta di dimensione binaria e di controllare che il tempo di caricamento / i-cache si comporti in modo simile. Ovviamente, meglio non colpire nessuno dei abort...
Quando la domanda è stata posta stavo andando dal dottore, con un taxi in attesa, quindi ho avuto solo il tempo per un breve commento. Ma avendo ora commentato e votato verso l'alto e verso il basso, farei meglio ad aggiungere la mia risposta. Anche se la risposta di Matthieu è già abbastanza buona.
Re il reclamo
"Stavo guardando Systematic Error Handling in C ++ - Andrei Alexandrescu afferma che le eccezioni in C ++ sono molto lente."
Se questo è letteralmente ciò che afferma Andrei, allora per una volta è molto fuorviante, se non addirittura sbagliato. Per le eccezioni sollevate / lanciate è sempre lento rispetto ad altre operazioni di base nel linguaggio, indipendentemente dal linguaggio di programmazione . Non solo in C ++ o più in C ++ che in altri linguaggi, come indica l'affermazione presunta.
In generale, per lo più indipendentemente dalla lingua, le due caratteristiche del linguaggio di base che sono ordini di grandezza più lente delle altre, perché si traducono in chiamate di routine che gestiscono strutture di dati complesse, sono
lancio di eccezioni e
allocazione dinamica della memoria.
Fortunatamente in C ++ si possono spesso evitare entrambi nel codice time-critical.
Sfortunatamente non esiste un pranzo gratuito , anche se l'efficienza predefinita del C ++ si avvicina molto. :-) L'efficienza ottenuta evitando il lancio di eccezioni e l'allocazione dinamica della memoria è generalmente ottenuta codificando a un livello inferiore di astrazione, usando C ++ solo come un "C migliore". E una minore astrazione significa maggiore "complessità".
Una maggiore complessità significa più tempo speso per la manutenzione e poco o nessun vantaggio dal riutilizzo del codice, che sono costi monetari reali, anche se difficili da stimare o misurare. Cioè, con C ++ si può, se lo si desidera, scambiare un po 'di efficienza del programmatore con l'efficienza di esecuzione. Se farlo è in gran parte una decisione ingegneristica e intuitiva, perché in pratica solo il guadagno, non il costo, può essere facilmente stimato e misurato.
Sì, il comitato internazionale di standardizzazione C ++ ha pubblicato un rapporto tecnico sulle prestazioni C ++, TR18015 .
Principalmente significa che un throwpuò impiegare molto tempo ™ rispetto ad esempio a un intincarico, a causa della ricerca di un conduttore.
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 tryblocco imposta dinamicamente il rilevamento delle eccezioni, in modo che una ricerca lungo la catena dinamica dei gestori venga eseguita quando viene generata un'eccezione, e
l'approccio in cui il compilatore genera tabelle di ricerca statiche utilizzate per determinare il gestore per un'eccezione generata.
Il primo approccio molto flessibile e generale è quasi forzato in Windows a 32 bit, mentre in land a 64 bit e in * nix-land viene comunemente utilizzato il secondo approccio molto più efficiente.
Inoltre, come discusso nel rapporto, per ogni approccio ci sono tre aree principali in cui la gestione delle eccezioni ha 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 tryblocchi, principalmente indipendentemente dalla lingua (perché questo è forzato dallo schema di gestione delle eccezioni strutturato di Windows ), mentre l'approccio della tabella statica ha un costo approssimativamente zero per try: blocchi. Discutere di questo richiederebbe molto più spazio e ricerca di quanto sia pratico per una risposta SO. Quindi, vedere il rapporto per i dettagli.
Sfortunatamente il rapporto, del 2006, è già un po 'datato alla fine del 2012 e per quanto ne so non c'è niente di paragonabile che sia più recente.
Un'altra prospettiva importante è che l'impatto dell'uso delle eccezioni sulle prestazioni è molto diverso dall'efficienza isolata delle funzionalità linguistiche di supporto, perché, come osserva il rapporto,
"Quando si considera la gestione delle eccezioni, è necessario confrontarla con metodi alternativi di gestione degli errori."
Per esempio:
Costi di manutenzione dovuti a diversi stili di programmazione (correttezza)
ifControllo degli errori del sito di chiamata ridondante rispetto a quello centralizzatotry
Problemi di memorizzazione nella cache (ad esempio, un codice più breve potrebbe entrare nella cache)
Il report ha un diverso elenco di aspetti da considerare, ma comunque l'unico modo pratico per ottenere dati concreti sull'efficienza di esecuzione è probabilmente quello di implementare lo stesso programma usando eccezioni e non usando eccezioni, entro un limite stabilito sui tempi di sviluppo e con gli sviluppatori familiarità con ogni modo, quindi MISURA .
La correttezza trionfa quasi sempre sull'efficienza.
Senza eccezioni, può facilmente accadere quanto segue:
Alcuni codici P hanno lo scopo di ottenere una risorsa o calcolare alcune informazioni.
Il codice chiamante C dovrebbe aver controllato per successo / fallimento, ma non lo fa.
Una risorsa inesistente o informazioni non valide vengono utilizzate nel codice che segue C, causando un caos generale.
Il problema principale è il punto (2), dove con il solito schema del codice di ritorno il codice chiamante C non è obbligato a controllare.
Esistono due approcci principali che impongono tale controllo:
Dove P genera direttamente un'eccezione quando fallisce.
Dove P restituisce un oggetto che C deve ispezionare prima di utilizzare il suo valore principale (altrimenti un'eccezione o una terminazione).
Il secondo approccio è stato, per quanto ne so, descritto per la prima volta da Barton e Nackman nel loro libro * Scientific and Engineering C ++: An Introduction with Advanced Techniques and examples , dove hanno introdotto una classe chiamata Fallowper un risultato di funzione "possibile". Una classe simile chiamata optionalè ora offerta dalla libreria Boost. E puoi facilmente implementare una Optionalclasse da solo, utilizzando un std::vectorvettore come valore per il caso di risultato non POD.
Con il primo approccio, il codice chiamante C non ha altra scelta che utilizzare tecniche di gestione delle eccezioni. Con il secondo approccio, tuttavia, il codice chiamante C può decidere se eseguire il ifcontrollo basato o la gestione delle eccezioni generali. Pertanto, il secondo approccio supporta il compromesso tra efficienza del programmatore e tempo di esecuzione.
"Voglio sapere se questo è ancora vero per C ++ 98"
C ++ 98 è stato il primo standard C ++. Per le eccezioni ha introdotto una gerarchia standard di classi di eccezioni (purtroppo piuttosto imperfetta). L'impatto principale sulle prestazioni è stata la possibilità di specifiche di eccezione (rimosse in C ++ 11), che tuttavia non sono mai state completamente implementate dal compilatore principale di Windows C ++ Visual C ++: Visual C ++ accetta la sintassi della specifica di eccezione C ++ 98, ma ignora semplicemente specifiche di eccezione.
C ++ 03 era solo una correzione tecnica di C ++ 98. L'unica novità in C ++ 03 è stata l' inizializzazione del valore . Che non ha nulla a che fare con le eccezioni.
Con lo standard C ++ 11 le specifiche delle eccezioni generali sono state rimosse e sostituite con la noexceptparola chiave.
Lo standard C ++ 11 ha anche aggiunto il supporto per l'archiviazione e il rilancio delle eccezioni, che è ottimo per propagare le eccezioni C ++ tra i callback del linguaggio C. Questo supporto limita in modo efficace la modalità di archiviazione dell'eccezione corrente. Tuttavia, per quanto ne so, ciò non influisce sulle prestazioni, tranne nella misura in cui nel nuovo codice la gestione delle eccezioni può essere più facilmente utilizzata su entrambi i lati di un callback in linguaggio C.
longjmpandare al gestore.
try..finallycostrutto può essere implementato senza lo svolgimento dello stack. F #, C # e Java vengono implementati try..finallysenza utilizzare lo svolgimento dello stack. Devi solo longjmpal conduttore (come ho già spiegato).
Non puoi mai rivendicare le prestazioni a meno che non converti il codice nell'assembly o lo benchmark.
Ecco cosa vedi: (panchina veloce)
Il codice di errore non è sensibile alla percentuale di occorrenza. Le eccezioni hanno un po 'di sovraccarico fintanto che non vengono mai lanciate. Una volta che li lanci, inizia la miseria. In questo esempio, viene lanciato per lo 0%, 1%, 10%, 50% e 90% dei casi. Quando le eccezioni vengono lanciate il 90% delle volte, il codice è 8 volte più lento rispetto al caso in cui le eccezioni vengono lanciate il 10% delle volte. Come vedi, le eccezioni sono davvero lente. Non usarli se vengono lanciati frequentemente. Se la tua applicazione non ha requisiti in tempo reale, sentiti libero di lanciarli se si verificano molto raramente.
Vedi molte opinioni contraddittorie su di loro. Ma infine, le eccezioni sono lente? Non giudico. Guarda il benchmark.
Dipende dal compilatore.
GCC, ad esempio, era noto per avere prestazioni molto scarse durante la gestione delle eccezioni, ma negli ultimi anni è migliorato notevolmente.
Ma si noti 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 da influire sulle prestazioni e questo è ancora considerato un funzionamento normale, è preferibile pensare a fare le cose in modo diverso.
Le eccezioni sono un ottimo modo per rendere il codice più leggibile eliminando tutto quel goffo codice di gestione degli errori, ma non appena diventano parte del normale flusso del programma, diventano davvero difficili da seguire. Ricorda che a throwè praticamente un goto catchtravestito.
throw new Exceptionè un Java-ismo. di regola non si dovrebbero mai lanciare indicatori.
Sì, ma non importa. Perché?
Leggi questo:
https://blogs.msdn.com/b/ericlippert/archive/2008/09/10/vexing-exceptions.aspx
Fondamentalmente questo dice che l'uso di eccezioni come descritto da Alexandrescu (rallentamento 50x perché usano catchas else) è semplicemente sbagliato. Detto questo per ppl a cui piace farlo in quel modo, vorrei che C ++ 22 :) aggiungesse qualcosa del tipo:
(nota che questo dovrebbe essere un linguaggio di base poiché è fondamentalmente il compilatore che genera codice da uno esistente)
result = attempt<lexical_cast<int>>("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 ... Ad esempio se la divisione in virgola mobile è lenta e la fai 4x più veloce che non importa se passi lo 0,3% del tuo tempo a fare la divisione FP ...
Come in silico, la sua implementazione dipende dall'implementazione, ma in generale le eccezioni sono considerate lente per qualsiasi implementazione e non dovrebbero essere utilizzate in codice ad alta intensità di prestazioni.
EDIT: Non sto dicendo di non usarli affatto, ma per il codice ad alte prestazioni è meglio evitarli.