I blocchi try / catch fanno male alle prestazioni quando non vengono lanciate eccezioni?

Durante una revisione del codice con un dipendente Microsoft ci siamo imbattuti in un’ampia sezione di codice all’interno di un blocco try{} . Lei e un rappresentante IT hanno suggerito che questo può avere effetti sulle prestazioni del codice. In realtà, hanno suggerito che la maggior parte del codice dovesse essere al di fuori dei blocchi try / catch e che solo le sezioni importanti dovrebbero essere controllate. Il dipendente Microsoft ha aggiunto e ha detto che un prossimo white paper mette in guardia contro i blocchi try / catch errati.

Mi sono guardato intorno e ho scoperto che può influire sulle ottimizzazioni , ma sembra che si applichi solo quando una variabile è condivisa tra gli ambiti.

Non mi sto chiedendo sulla manutenibilità del codice, o anche sulla gestione delle giuste eccezioni (il codice in questione ha bisogno di ri-factoring, senza dubbio). Inoltre, non mi riferisco all’utilizzo di eccezioni per il controllo del stream, questo è chiaramente sbagliato nella maggior parte dei casi. Questi sono problemi importanti (alcuni sono più importanti), ma non l’attenzione qui.

In che modo i blocchi try / catch influiscono sulle prestazioni quando non vengono lanciate eccezioni?

EDIT: sto aggiungendo una taglia. Ci sono risposte interessanti, ma mi piacerebbe ottenere qualche input in più.

Controllalo.

 static public void Main(string[] args) { Stopwatch w = new Stopwatch(); double d = 0; w.Start(); for (int i = 0; i < 10000000; i++) { try { d = Math.Sin(1); } catch (Exception ex) { Console.WriteLine(ex.ToString()); } } w.Stop(); Console.WriteLine(w.Elapsed); w.Reset(); w.Start(); for (int i = 0; i < 10000000; i++) { d = Math.Sin(1); } w.Stop(); Console.WriteLine(w.Elapsed); } 

Produzione:

 00:00:00.4269033 // with try/catch 00:00:00.4260383 // without. 

In millisecondi:

 449 416 

Nuovo codice:

 for (int j = 0; j < 10; j++) { Stopwatch w = new Stopwatch(); double d = 0; w.Start(); for (int i = 0; i < 10000000; i++) { try { d = Math.Sin(d); } catch (Exception ex) { Console.WriteLine(ex.ToString()); } finally { d = Math.Sin(d); } } w.Stop(); Console.Write(" try/catch/finally: "); Console.WriteLine(w.ElapsedMilliseconds); w.Reset(); d = 0; w.Start(); for (int i = 0; i < 10000000; i++) { d = Math.Sin(d); d = Math.Sin(d); } w.Stop(); Console.Write("No try/catch/finally: "); Console.WriteLine(w.ElapsedMilliseconds); Console.WriteLine(); } 

Nuovi risultati:

  try/catch/finally: 382 No try/catch/finally: 332 try/catch/finally: 375 No try/catch/finally: 332 try/catch/finally: 376 No try/catch/finally: 333 try/catch/finally: 375 No try/catch/finally: 330 try/catch/finally: 373 No try/catch/finally: 329 try/catch/finally: 373 No try/catch/finally: 330 try/catch/finally: 373 No try/catch/finally: 352 try/catch/finally: 374 No try/catch/finally: 331 try/catch/finally: 380 No try/catch/finally: 329 try/catch/finally: 374 No try/catch/finally: 334 

Dopo aver visto tutte le statistiche con try / catch e senza try / catch, la curiosità mi ha costretto a guardare dietro per vedere cosa viene generato per entrambi i casi. Ecco il codice:

C #:

 private static void TestWithoutTryCatch(){ Console.WriteLine("SIN(1) = {0} - No Try/Catch", Math.Sin(1)); } 

MSIL:

 .method private hidebysig static void TestWithoutTryCatch() cil managed { // Code size 32 (0x20) .maxstack 8 IL_0000: nop IL_0001: ldstr "SIN(1) = {0} - No Try/Catch" IL_0006: ldc.r8 1. IL_000f: call float64 [mscorlib]System.Math::Sin(float64) IL_0014: box [mscorlib]System.Double IL_0019: call void [mscorlib]System.Console::WriteLine(string, object) IL_001e: nop IL_001f: ret } // end of method Program::TestWithoutTryCatch 

C #:

 private static void TestWithTryCatch(){ try{ Console.WriteLine("SIN(1) = {0}", Math.Sin(1)); } catch (Exception ex){ Console.WriteLine(ex); } } 

MSIL:

 .method private hidebysig static void TestWithTryCatch() cil managed { // Code size 49 (0x31) .maxstack 2 .locals init ([0] class [mscorlib]System.Exception ex) IL_0000: nop .try { IL_0001: nop IL_0002: ldstr "SIN(1) = {0}" IL_0007: ldc.r8 1. IL_0010: call float64 [mscorlib]System.Math::Sin(float64) IL_0015: box [mscorlib]System.Double IL_001a: call void [mscorlib]System.Console::WriteLine(string, object) IL_001f: nop IL_0020: nop IL_0021: leave.s IL_002f //JUMP IF NO EXCEPTION } // end .try catch [mscorlib]System.Exception { IL_0023: stloc.0 IL_0024: nop IL_0025: ldloc.0 IL_0026: call void [mscorlib]System.Console::WriteLine(object) IL_002b: nop IL_002c: nop IL_002d: leave.s IL_002f } // end handler IL_002f: nop IL_0030: ret } // end of method Program::TestWithTryCatch 

Non sono un esperto di IL ma possiamo vedere che un object di eccezione locale viene creato sulla quarta riga .locals init ([0] class [mscorlib]System.Exception ex) dopo che le cose sono praticamente le stesse del metodo senza try / prendere fino alla linea diciassette IL_0021: leave.s IL_002f . Se si verifica un’eccezione, il controllo salta sulla riga IL_0025: ldloc.0 altrimenti passiamo all’etichetta IL_002d: leave.s IL_002f e la funzione restituisce.

Posso tranquillamente supporre che se non si verificano eccezioni, è il sovraccarico di creare variabili locali per contenere solo oggetti di eccezione e un’istruzione di salto.

No. Se le ottimizzazioni insignificanti che un blocco try / finally preclude effettivamente hanno un impatto misurabile sul tuo programma, probabilmente non dovresti utilizzare .NET in primo luogo.

Una spiegazione abbastanza completa del modello di eccezioni .NET.

Tidbits delle prestazioni di Rico Mariani: Costo delle eccezioni: quando lanciare e quando no

Il primo tipo di costo è il costo statico della gestione delle eccezioni nel codice. Le eccezioni gestite in realtà fanno relativamente bene qui, con il quale intendo che il costo statico può essere molto più basso di quello del C ++. Perchè è questo? Bene, il costo statico è realmente sostenuto in due tipi di posti: in primo luogo, i siti reali di try / finally / catch / throw dove c’è il codice per quei costrutti. Secondo, nel codice non modificato, c’è il costo nascosto associato alla traccia di tutti gli oggetti che devono essere distrutti nel caso in cui venga lanciata un’eccezione. C’è una notevole quantità di logica di pulizia che deve essere presente e la parte subdola è che anche il codice che non lancia o cattura da sé o che altrimenti ha un uso evidente delle eccezioni porta ancora il peso di sapere come ripulire se stesso.

Dmitriy Zaslavskiy:

Come nota di Chris Brumme: C’è anche un costo legato al fatto che alcune ottimizzazioni non vengono eseguite da JIT in presenza di catture

La struttura è diversa nell’esempio di Ben M. Sarà esteso in testa all’interno del ciclo for interno che non farà un buon confronto tra i due casi.

Quanto segue è più accurato per il confronto in cui l’intero codice da controllare (inclusa la dichiarazione delle variabili) si trova all’interno del blocco Try / Catch:

  for (int j = 0; j < 10; j++) { Stopwatch w = new Stopwatch(); w.Start(); try { double d1 = 0; for (int i = 0; i < 10000000; i++) { d1 = Math.Sin(d1); d1 = Math.Sin(d1); } } catch (Exception ex) { Console.WriteLine(ex.ToString()); } finally { //d1 = Math.Sin(d1); } w.Stop(); Console.Write(" try/catch/finally: "); Console.WriteLine(w.ElapsedMilliseconds); w.Reset(); w.Start(); double d2 = 0; for (int i = 0; i < 10000000; i++) { d2 = Math.Sin(d2); d2 = Math.Sin(d2); } w.Stop(); Console.Write("No try/catch/finally: "); Console.WriteLine(w.ElapsedMilliseconds); Console.WriteLine(); } 

Quando ho eseguito il codice di test originale da Ben M , ho notato una differenza sia nella configurazione di Debug che in quella di Releas.

Questa versione, ho notato una differenza nella versione di debug (in realtà più dell'altra versione), ma non c'era alcuna differenza nella versione Release.

Conclution :
Sulla base di questi test, penso che possiamo dire che Try / Catch ha un piccolo impatto sulle prestazioni.

MODIFICARE:
Ho provato ad aumentare il valore del loop da 10000000 a 1000000000 e ho eseguito nuovamente in Release per ottenere alcune differenze nel rilascio, e il risultato è stato questo:

  try/catch/finally: 509 No try/catch/finally: 486 try/catch/finally: 479 No try/catch/finally: 511 try/catch/finally: 475 No try/catch/finally: 477 try/catch/finally: 477 No try/catch/finally: 475 try/catch/finally: 475 No try/catch/finally: 476 try/catch/finally: 477 No try/catch/finally: 474 try/catch/finally: 475 No try/catch/finally: 475 try/catch/finally: 476 No try/catch/finally: 476 try/catch/finally: 475 No try/catch/finally: 476 try/catch/finally: 475 No try/catch/finally: 474 

Vedi che il risultato è irrilevante. In alcuni casi la versione che utilizza Try / Catch è in realtà più veloce!

Ho provato l’impatto reale di un try..catch in un loop stretto, ed è troppo piccolo da solo per essere un problema di prestazioni in qualsiasi situazione normale.

Se il ciclo fa pochissimo lavoro (nel mio test ho fatto un x++ ), puoi misurare l’impatto della gestione delle eccezioni. Il ciclo con gestione delle eccezioni è durato circa dieci volte di più.

Se il ciclo esegue un lavoro effettivo (nel mio test ho chiamato il metodo Int32.Parse), la gestione delle eccezioni ha un impatto troppo piccolo per essere misurabile. Ho ottenuto una differenza molto più grande scambiando l’ordine dei loop …

prova che i blocchi di cattura hanno un impatto trascurabile sulle prestazioni ma eccezione Il lancio può essere piuttosto considerevole, questo è probabilmente il punto in cui il tuo collega è stato confuso.

Il try / catch ha un impatto sulla performance.

Ma non è un impatto enorme. la complessità try / catch è generalmente O (1), proprio come un semplice compito, tranne quando sono posizionati in un ciclo. Quindi devi usarli saggiamente.

Ecco un riferimento a prova / cattura delle prestazioni (non spiega la complessità di esso, ma è implicito). Dai un’occhiata alla sezione Throw Fewer Exceptions

In teoria, un blocco try / catch non avrà alcun effetto sul comportamento del codice a meno che non si verifichi effettivamente un’eccezione. Ci sono alcune rare circostanze, tuttavia, in cui l’esistenza di un blocco try / catch può avere un effetto maggiore, e alcuni non comuni, ma difficilmente oscuri, in cui l’effetto può essere evidente. La ragione di questo è che il codice dato come:

 Action q; double thing1() { double total; for (int i=0; i<1000000; i++) total+=1.0/i; return total;} double thing2() { q=null; return 1.0;} ... x=thing1(); // statement1 x=thing2(x); // statement2 doSomething(x); // statement3 

il compilatore potrebbe essere in grado di ottimizzare statement1 in base al fatto che l'istruzione2 è garantita per l'esecuzione prima dell'istruzione3. Se il compilatore è in grado di riconoscere quella cosa1 non ha effetti collaterali e thing2 in realtà non usa x, può tranquillamente omettere la cosa1 del tutto. Se [come in questo caso] la cosa1 fosse costosa, quella potrebbe essere una big ottimizzazione, anche se i casi in cui cosa1 è costosa sono anche quelli che il compilatore avrebbe meno probabilità di ottimizzare. Supponiamo che il codice sia stato cambiato:

 x=thing1(); // statement1 try { x=thing2(x); } // statement2 catch { q(); } doSomething(x); // statement3 

Ora esiste una sequenza di eventi in cui statement3 potrebbe eseguire senza aver eseguito statement_2. Anche se nulla nel codice per thing2 potrebbe generare un'eccezione, sarebbe ansible che un altro thread possa utilizzare un Interlocked.CompareExchange per notare che q stato cancellato e impostarlo su Thread.ResetAbort , quindi eseguire un Thread.Abort() prima di statement2 ha scritto il suo valore in x . Quindi il catch eseguirà Thread.ResetAbort() [via delegate q ], permettendo all'esecuzione di continuare con statement3. Una simile sequenza di eventi sarebbe ovviamente eccezionalmente improbabile, ma è necessario un compilatore per generare codice che funzioni secondo le specifiche anche quando si verificano tali eventi improbabili.

In generale, è molto più probabile che il compilatore noti le opportunità di tralasciare bit di codice semplici rispetto a quelli complessi, e quindi sarebbe raro che un try / catch possa influenzare molto le prestazioni se non vengono mai lanciate eccezioni. Tuttavia, ci sono alcune situazioni in cui l'esistenza di un blocco try / catch può impedire ottimizzazioni che - ma per il try / catch - avrebbero permesso al codice di funzionare più velocemente.

Vedere la discussione sull’implementazione try / catch per una discussione su come funzionano i blocchi try / catch e su come alcune implementazioni hanno un overhead elevato e alcune hanno un overhead zero, quando non si verificano eccezioni. In particolare, penso che l’implementazione di Windows a 32 bit abbia un overhead elevato e l’implementazione a 64 bit no.