Quanto lente sono le eccezioni .NET?

Non voglio una discussione su quando e non fare eccezioni. Desidero risolvere un semplice problema. Il 99% delle volte l’argomento per non generare eccezioni ruota intorno a loro mentre è lento mentre l’altra parte afferma (con test di benchmark) che la velocità non è il problema. Ho letto numerosi blog, articoli e post riguardanti una parte o l’altra. Quindi qual è?

Alcuni collegamenti dalle risposte: Skeet , Mariani , Brumme .

Sono sul lato “non lento” – o più precisamente “non abbastanza lento da far valere la pena di evitarli nel normale uso”. Ho scritto due brevi articoli su questo. Ci sono critiche sull’aspetto del benchmark, che sono per lo più “nella vita reale ci sarebbe più stack da passare, quindi faresti saltare la cache ecc.” Ma usare i codici di errore per farti strada nello stack sarebbe anche soffiare il cache, quindi non lo vedo come argomento particolarmente valido.

Giusto per chiarire: non supporto l’utilizzo di eccezioni dove non sono logiche. Ad esempio, int.TryParse è del tutto appropriato per la conversione di dati da un utente. È appropriato quando si legge un file generato dalla macchina, in cui un errore significa “Il file non è nel formato che è destinato a essere, non voglio davvero provare a gestirlo perché non so che altro potrebbe essere sbagliato. ”

Quando si utilizzano le eccezioni in “solo circostanze ragionevoli”, non ho mai visto un’applicazione la cui performance fosse significativamente compromise dalle eccezioni. Fondamentalmente, le eccezioni non dovrebbero accadere spesso a meno che tu non abbia problemi di correttezza significativi, e se hai problemi di correttezza significativa, le prestazioni non sono il problema più grande che affronti.

C’è una risposta definitiva a questo dal ragazzo che li ha implementati – Chris Brumme. Ha scritto un eccellente articolo sul blog (attenzione – è molto lungo) (warning2 – è scritto molto bene, se sei un tecnico lo leggi fino alla fine e poi devi recuperare le ore dopo il lavoro 🙂 )

Il sumrio esecutivo: sono lenti. Sono implementati come eccezioni di Win32 SEH, quindi alcuni supereranno anche il limite della CPU dello zero! Ovviamente nel mondo reale farai molti altri lavori, quindi l’eccezione dispari non verrà notata affatto, ma se li usi per il stream del programma, eccetto per la tua app da battere. Questo è un altro esempio della macchina di marketing MS che ci sta facendo un cattivo servizio. Ricordo una microsofia che ci dice come si sono verificati assolutamente zero overhead, che è completo tosh.

Chris fornisce una citazione pertinente:

In effetti, il CLR utilizza internamente eccezioni anche nelle parti non gestite del motore. Tuttavia, vi è un serio problema di prestazioni a lungo termine con le eccezioni e questo deve essere preso in considerazione nella tua decisione.

Non ho idea di cosa stiano parlando le persone quando dicono che sono lente solo se vengono lanciate.

EDIT: se le eccezioni non vengono lanciate, significa che stai facendo una nuova eccezione () o qualcosa del genere. Altrimenti, l’eccezione causerà la sospensione del thread e lo spostamento della pila. Questo potrebbe essere Ok in situazioni più piccole, ma nei siti Web ad alto traffico, fare affidamento sulle eccezioni come un stream di lavoro o un meccanismo del percorso di esecuzione causerà senz’altro problemi di prestazioni. Le eccezioni, di per sé, non sono male e sono utili per esprimere condizioni eccezionali

Il stream di lavoro di eccezione in un’app .NET utilizza le eccezioni di prima e seconda possibilità. Per tutte le eccezioni, anche se le stai prendendo e gestendole, l’object eccezione viene comunque creato e il framework deve ancora percorrere lo stack per cercare un gestore. Se prendi e rilancia, naturalmente, ci vorrà più tempo – otterrai un’eccezione di prima scelta, prendila, rilanciala, causando un’altra eccezione di prima possibilità, che quindi non trova un gestore, che quindi causa una seconda eccezione di possibilità.

Le eccezioni sono anche oggetti nell’heap, quindi se si generano tonnellate di eccezioni, si verificano problemi di prestazioni e di memoria.

Inoltre, secondo la mia copia di “Performance Testing Microsoft .NET Web Applications” scritta dal team ACE:

“La gestione delle eccezioni è costosa, l’esecuzione del thread implicato è sospesa mentre CLR ricorre attraverso lo stack di chiamate alla ricerca del gestore delle eccezioni corretto, e quando viene trovato, il gestore delle eccezioni e un certo numero di blocchi finali devono avere tutte le possibilità di eseguire prima che l’elaborazione normale possa essere eseguita. ”

La mia esperienza sul campo ha dimostrato che la riduzione delle eccezioni ha contribuito significativamente alla performance. Naturalmente, ci sono altre cose che si prendono in considerazione quando si esegue il test delle prestazioni – ad esempio, se viene eseguito il backup dell’I / O del disco o se le query sono nei secondi, questo dovrebbe essere il tuo objective. Ma trovare e rimuovere le eccezioni dovrebbe essere una parte vitale di questa strategia.

L’argomento, a quanto ho capito, non è che le eccezioni di lancio sono cattive perché sono lente di per sé. Invece, si tratta di usare il costrutto throw / catch come un modo di prima class per controllare la normale logica applicativa, invece di costrutti condizionali più tradizionali.

Spesso nella normale logica applicativa si esegue il ciclo in cui la stessa azione viene ripetuta migliaia / milioni di volte. In questo caso, con una semplice profilazione (vedi la lezione di Cronometro), puoi vedere da te che lanciare un’eccezione invece di dire una semplice dichiarazione if può risultare sostanzialmente più lenta.

Infatti, una volta ho letto che il team .NET di Microsoft ha introdotto i metodi TryXXXXX in .NET 2.0 su molti tipi di FCL di base, in particolare perché i clienti si lamentavano del fatto che le prestazioni delle loro applicazioni fossero così lente.

In molti casi ciò si è verificato perché i clienti tentavano di convertire i valori in un ciclo e ogni tentativo non è riuscito. Un’eccezione di conversione è stata generata e quindi catturata da un gestore di eccezioni che ha in seguito inghiottito l’eccezione e ha continuato il ciclo.

Microsoft ora consiglia di utilizzare i metodi TryXXX in particolare in questa situazione per evitare tali possibili problemi di prestazioni.

Potrei sbagliarmi, ma sembra che tu non sia certo della veridicità dei “parametri di riferimento” di cui hai letto. Soluzione semplice: provalo tu stesso.

Il mio server XMPP ha ottenuto un notevole aumento di velocità (scusate, nessun numero effettivo, puramente osservativo) dopo che ho costantemente cercato di impedire che accadessero (come controllare se un socket è connesso prima di provare a leggere più dati) e darmi dei modi per evitarli (i metodi TryX citati). Era solo con circa 50 utenti virtuali attivi (in chat).

Non ho mai avuto problemi di prestazioni con le eccezioni. Uso molto le eccezioni – non uso mai i codici di ritorno se posso. Sono una ctriggers pratica e, secondo me, odorano di codice spaghetti.

Penso che tutto si riduce a come si usano le eccezioni: se le si usano come codici di ritorno (ogni chiamata di metodo nello stack cattura e retrocede), sì, saranno lenti, perché si ha un sovraccarico ogni singolo catch / throw.

Ma se si lancia in fondo alla pila e si cattura in alto (si sostituisce un’intera catena di codici di ritorno con un tiro / presa), tutte le operazioni costose vengono eseguite una volta.

Alla fine della giornata, sono una funzionalità linguistica valida.

Solo per dimostrare il mio punto

Si prega di eseguire il codice a questo link (troppo grande per una risposta).

Risultati sul mio computer:

marco@sklivvz:~/develop/test$ mono Exceptions.exe | grep PM
10/2/2008 2:53:32 PM
10/2/2008 2:53:42 PM
10/2/2008 2:53:52 PM

I timestamp vengono emessi all’inizio, tra codici di ritorno ed eccezioni, alla fine. Prende lo stesso tempo in entrambi i casi. Si noti che è necessario compilare con le ottimizzazioni.

Se li confronti per restituire i codici sono lenti come diavolo. Tuttavia, poiché i poster precedenti affermavano che non si desidera avviare il normale funzionamento del programma in modo da ottenere il colpo perfetto solo quando si verifica un problema e nella stragrande maggioranza dei casi le prestazioni non contano più (poiché l’eccezione implica comunque un blocco stradale).

Vale sicuramente la pena di usare i codici di errore, i vantaggi sono immensi.

Giusto per aggiungere la mia esperienza recente a questa discussione: in linea con la maggior parte di quanto scritto sopra, ho trovato che le eccezioni di lancio sono estremamente lente se eseguite su base ripetuta, anche senza il debugger in esecuzione. Ho appena aumentato le prestazioni di un grande programma che sto scrivendo del 60% modificando circa cinque righe di codice: passando a un modello di codice di ritorno invece di generare eccezioni. Certo, il codice in questione era in esecuzione migliaia di volte e potenzialmente gettava migliaia di eccezioni prima di cambiarlo. Quindi sono d’accordo con l’affermazione precedente: lanciare eccezioni quando qualcosa di importante in realtà va storto, non come un modo per controllare il stream delle applicazioni in qualsiasi situazione “attesa”.

Ma mono getta un’eccezione 10 volte più veloce della modalità .net standalone e la modalità standalone .net fa eccezione a 60 volte più veloce della modalità debug .net. (Le macchine di prova hanno lo stesso modello di CPU)

 int c = 1000000; int s = Environment.TickCount; for (int i = 0; i < c; i++) { try { throw new Exception(); } catch { } } int d = Environment.TickCount - s; Console.WriteLine(d + "ms / " + c + " exceptions"); 

Nella modalità di rilascio il sovraccarico è minimo.

A meno che non si stiano utilizzando le eccezioni per il controllo del stream (esempio, le uscite non locali) in modo ricorsivo, dubito che si sarà in grado di notare la differenza.

Nel CLR di Windows, per una catena di chiamate depth-8, il lancio di un’eccezione è 750 volte più lento del controllo e della propagazione di un valore di ritorno. (vedi sotto per i benchmark)

Questo costo elevato per le eccezioni è dovuto al fatto che Windows CLR si integra con qualcosa chiamato Windows Structured Exception Handling . Ciò consente alle eccezioni di essere correttamente catturate e lanciate attraverso diversi runtime e lingue. Tuttavia, è molto molto lento.

Le eccezioni nel runtime Mono (su qualsiasi piattaforma) sono molto più veloci, perché non si integrano con SEH. Tuttavia, si verifica una perdita di funzionalità quando si passano eccezioni su più runtime perché non utilizza nulla come SEH.

Ecco i risultati abbreviati del mio benchmark di eccezioni rispetto ai valori di ritorno per il CLR di Windows.

 baseline: recurse_depth 8, error_freqeuncy 0 (0), time elapsed 13.0007 ms baseline: recurse_depth 8, error_freqeuncy 0.25 (0), time elapsed 13.0007 ms baseline: recurse_depth 8, error_freqeuncy 0.5 (0), time elapsed 13.0008 ms baseline: recurse_depth 8, error_freqeuncy 0.75 (0), time elapsed 13.0008 ms baseline: recurse_depth 8, error_freqeuncy 1 (0), time elapsed 14.0008 ms retval_error: recurse_depth 5, error_freqeuncy 0 (0), time elapsed 13.0008 ms retval_error: recurse_depth 5, error_freqeuncy 0.25 (249999), time elapsed 14.0008 ms retval_error: recurse_depth 5, error_freqeuncy 0.5 (499999), time elapsed 16.0009 ms retval_error: recurse_depth 5, error_freqeuncy 0.75 (999999), time elapsed 16.001 ms retval_error: recurse_depth 5, error_freqeuncy 1 (999999), time elapsed 16.0009 ms retval_error: recurse_depth 8, error_freqeuncy 0 (0), time elapsed 20.0011 ms retval_error: recurse_depth 8, error_freqeuncy 0.25 (249999), time elapsed 21.0012 ms retval_error: recurse_depth 8, error_freqeuncy 0.5 (499999), time elapsed 24.0014 ms retval_error: recurse_depth 8, error_freqeuncy 0.75 (999999), time elapsed 24.0014 ms retval_error: recurse_depth 8, error_freqeuncy 1 (999999), time elapsed 24.0013 ms exception_error: recurse_depth 8, error_freqeuncy 0 (0), time elapsed 31.0017 ms exception_error: recurse_depth 8, error_freqeuncy 0.25 (249999), time elapsed 5607.3208 ms exception_error: recurse_depth 8, error_freqeuncy 0.5 (499999), time elapsed 11172.639 ms exception_error: recurse_depth 8, error_freqeuncy 0.75 (999999), time elapsed 22297.2753 ms exception_error: recurse_depth 8, error_freqeuncy 1 (999999), time elapsed 22102.2641 ms 

Ed ecco il codice ..

 using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ConsoleApplication1 { public class TestIt { int value; public class TestException : Exception { } public int getValue() { return value; } public void reset() { value = 0; } public bool baseline_null(bool shouldfail, int recurse_depth) { if (recurse_depth < = 0) { return shouldfail; } else { return baseline_null(shouldfail,recurse_depth-1); } } public bool retval_error(bool shouldfail, int recurse_depth) { if (recurse_depth <= 0) { if (shouldfail) { return false; } else { return true; } } else { bool nested_error = retval_error(shouldfail,recurse_depth-1); if (nested_error) { return true; } else { return false; } } } public void exception_error(bool shouldfail, int recurse_depth) { if (recurse_depth <= 0) { if (shouldfail) { throw new TestException(); } } else { exception_error(shouldfail,recurse_depth-1); } } public static void Main(String[] args) { int i; long l; TestIt t = new TestIt(); int failures; int ITERATION_COUNT = 1000000; // (0) baseline null workload for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) { for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) { int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq); failures = 0; DateTime start_time = DateTime.Now; t.reset(); for (i = 1; i < ITERATION_COUNT; i++) { bool shoulderror = (i % EXCEPTION_MOD) == 0; t.baseline_null(shoulderror,recurse_depth); } double elapsed_time = (DateTime.Now - start_time).TotalMilliseconds; Console.WriteLine( String.Format( "baseline: recurse_depth {0}, error_freqeuncy {1} ({2}), time elapsed {3} ms", recurse_depth, exception_freq, failures,elapsed_time)); } } // (1) retval_error for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) { for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) { int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq); failures = 0; DateTime start_time = DateTime.Now; t.reset(); for (i = 1; i < ITERATION_COUNT; i++) { bool shoulderror = (i % EXCEPTION_MOD) == 0; if (!t.retval_error(shoulderror,recurse_depth)) { failures++; } } double elapsed_time = (DateTime.Now - start_time).TotalMilliseconds; Console.WriteLine( String.Format( "retval_error: recurse_depth {0}, error_freqeuncy {1} ({2}), time elapsed {3} ms", recurse_depth, exception_freq, failures,elapsed_time)); } } // (2) exception_error for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) { for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) { int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq); failures = 0; DateTime start_time = DateTime.Now; t.reset(); for (i = 1; i < ITERATION_COUNT; i++) { bool shoulderror = (i % EXCEPTION_MOD) == 0; try { t.exception_error(shoulderror,recurse_depth); } catch (TestException e) { failures++; } } double elapsed_time = (DateTime.Now - start_time).TotalMilliseconds; Console.WriteLine( String.Format( "exception_error: recurse_depth {0}, error_freqeuncy {1} ({2}), time elapsed {3} ms", recurse_depth, exception_freq, failures,elapsed_time)); } } } } } 

Una breve nota qui sulle prestazioni associate alla cattura delle eccezioni.

Quando il percorso di esecuzione entra in un blocco ‘prova’, non accade nulla di magico. Non esiste un’istruzione ‘try’ e nessun costo associato all’inserimento o all’uscita dal blocco try. Le informazioni sul blocco try vengono archiviate nei metadati del metodo e questi metadati vengono utilizzati in fase di esecuzione ogni volta che viene sollevata un’eccezione. Il motore di esecuzione cammina lungo lo stack cercando la prima chiamata contenuta in un blocco try. Qualsiasi sovraccarico associato alla gestione delle eccezioni si verifica solo quando vengono lanciate eccezioni.

Quando si scrivono classi / funzioni affinché altri possano usarlo sembra difficile dire quando le eccezioni sono appropriate. Ci sono alcune parti utili di BCL che ho dovuto abbandonare e andare per Pinvoke perché generano eccezioni invece di restituire errori. In alcuni casi è ansible aggirare il problema, ma per altri come System.Management e Performance Counters ci sono usi in cui è necessario fare cicli in cui le eccezioni vengono lanciate frequentemente da BCL.

Se stai scrivendo una libreria e c’è una remota possibilità che la tua funzione possa essere usata in un ciclo e c’è un potenziale per una grande quantità di iterazioni, usa il pattern Try .. o un altro modo per esporre gli errori accanto alle eccezioni. E anche allora, è difficile dire quanto verrà chiamata la funzione se viene utilizzata da molti processi in ambiente condiviso.

Nel mio codice, le eccezioni vengono lanciate solo quando le cose sono così eccezionali che è necessario andare a esaminare la traccia dello stack e vedere cosa è andato storto e poi ripararlo. Quindi ho praticamente riscritto parti di BCL per utilizzare la gestione degli errori basata su Try .. pattern anziché su eccezioni.