I blocchi di prova / cattura danneggiano le prestazioni quando non vengono generate eccezioni?


274

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

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

Non sto chiedendo della manutenibilità del codice, o nemmeno gestendo le giuste eccezioni (il codice in questione ha bisogno di essere ripreso, senza dubbio). Inoltre non mi riferisco all'utilizzo di eccezioni per il controllo del flusso, questo è chiaramente sbagliato nella maggior parte dei casi. Quelle sono questioni importanti (alcune sono più importanti), ma non l'attenzione qui.

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


147
"Chi non sacrifica la correttezza per le prestazioni non merita nessuno dei due".
Joel Coehoorn,

16
detto ciò, la correttezza non deve sempre essere sacrificata per le prestazioni.
Dan Davies Brackett

19
Che ne dici di semplice curiosità?
Samantha Branham

63
@Joel: forse Kobi vuole solo conoscere la risposta per curiosità. Sapere se le prestazioni saranno migliori o peggiori non significa necessariamente che farà qualcosa di folle con il suo codice. La ricerca della conoscenza per se stessa non è una buona cosa?
LukeH

6
Ecco un buon algoritmo per sapere se apportare questa modifica o meno. Innanzitutto, impostare obiettivi di prestazione significativi basati sul cliente. Secondo, scrivi prima il codice per essere sia corretto che chiaro. Terzo, testalo contro i tuoi obiettivi. In quarto luogo, se raggiungi i tuoi obiettivi, togli il lavoro presto e vai in spiaggia. In quinto luogo, se non raggiungi i tuoi obiettivi, usa un profiler per trovare il codice che è troppo lento. In sesto luogo, se tale codice risulta essere troppo lento a causa di un gestore di eccezioni non necessario, rimuovere solo il gestore di eccezioni. In caso contrario, correggere il codice che in realtà è troppo lento. Quindi tornare al passaggio tre.
Eric Lippert,

Risposte:


203

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

24
Puoi provarli anche in ordine inverso per essere sicuro che la compilazione JIT non abbia avuto effetto sul primo?
JoshJordan,

28
Programmi come questo difficilmente sembrano buoni candidati per testare l'impatto della gestione delle eccezioni, troppo di quello che succederebbe nei normali blocchi di prova {} catch {} verrà ottimizzato. Potrei essere fuori a pranzo per quello ...
LorenVS,

30
Questa è una build di debug. JIT non li ottimizza.
Ben M,

7
Questo non è affatto vero, pensaci. Quante volte che usi prova a catturare in un ciclo? Il più delle volte utilizzerai il loop in un try.c
Athiwat Chunlakhan,

9
Veramente? "In che modo i blocchi try / catch influiscono sulle prestazioni quando non vengono generate eccezioni?"
Ben M,

105

Dopo aver visto tutte le statistiche per 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 oggetto di eccezione locale viene creato sulla quarta riga .locals init ([0] class [mscorlib]System.Exception ex)dopo che le cose sono piuttosto le stesse del metodo senza provare / catturare fino alla diciassette riga IL_0021: leave.s IL_002f. Se si verifica un'eccezione, il controllo passa alla riga, IL_0025: ldloc.0altrimenti passiamo all'etichetta IL_002d: leave.s IL_002fe la funzione ritorna.

Posso tranquillamente presumere che se non si verificano eccezioni, è il sovraccarico della creazione di variabili locali per contenere solo oggetti eccezione e un'istruzione jump.


33
Bene, IL include un blocco try / catch nella stessa notazione di C #, quindi questo non mostra davvero quanto overhead significhi un tentativo / catch dietro le quinte! Solo che IL non aggiunge molto di più, non significa lo stesso in quanto non viene aggiunto qualcosa nel codice assembly compilato. IL è solo una rappresentazione comune di tutti i linguaggi .NET. NON è un codice macchina!
timore re

64

No. Se le banali ottimizzazioni che un blocco try / finally preclude in realtà hanno un impatto misurabile sul programma, probabilmente non dovresti usare .NET in primo luogo.


10
Questo è un punto eccellente: confrontalo con gli altri elementi del nostro elenco, questo dovrebbe essere minuscolo. Dobbiamo fidarci delle funzionalità del linguaggio di base per comportarci correttamente e ottimizzare ciò che possiamo controllare (sql, indici, algoritmi).
Kobi,

3
Pensa ad anelli stretti amico. Esempio il ciclo in cui leggi e deserializzi gli oggetti da un flusso di dati socket nel server di gioco e provi a spremere il più possibile. Quindi MessagePack per la serializzazione di oggetti invece di binaryformatter e usi ArrayPool <byte> invece di creare semplicemente array di byte, ecc ... In questi scenari qual è l'impatto di più (forse nidificati) provare a catturare blocchi all'interno del loop stretto. Alcune ottimizzazioni verranno ignorate dal compilatore anche la variabile di eccezione va a Gen0 GC. Tutto quello che sto dicendo è che ci sono scenari "Alcuni" in cui tutto ha un impatto.
tcwicks,

35

Spiegazione abbastanza completa del modello di eccezione .NET.

Tidbits sulle prestazioni di Rico Mariani: costo eccezionale: quando lanciare e quando non farlo

Il primo tipo di costo è il costo statico della gestione delle eccezioni nel codice. Le eccezioni gestite in realtà funzionano relativamente bene qui, il che significa che il costo statico può essere molto più basso di quanto si pensi in C ++. Perchè è questo? Bene, il costo statico è realmente sostenuto in due tipi di luoghi: in primo luogo, i siti reali di try / finally / catch / throw dove c'è codice per quei costrutti. In secondo luogo, nel codice non modificato, c'è il costo invisibile associato al tenere traccia di tutti gli oggetti che devono essere distrutti nel caso in cui venga generata un'eccezione. C'è una notevole quantità di logica di pulizia che deve essere presente e la parte subdola è che anche il codice che non

Dmitriy Zaslavskiy:

Secondo la nota di Chris Brumme: esiste anche un costo legato al fatto che alcune ottimizzazioni non vengono eseguite da JIT in presenza di catture


1
La cosa su C ++ è che un grosso pezzo della libreria standard genererà eccezioni. Non c'è nulla di facoltativo su di loro. Devi progettare i tuoi oggetti con una sorta di politica di eccezione e, una volta fatto, non ci saranno più costi invisibili.
David Thornley,

Le affermazioni di Rico Mariani sono completamente errate per il C ++ nativo. "il costo statico può essere molto più basso di quello che si dice in C ++" - Semplicemente non è vero. Tuttavia, non sono sicuro di quale sia stato il design del meccanismo di eccezione nel 2003 quando l'articolo è stato scritto. Il C ++ non ha alcun costo se non vengono generate eccezioni , indipendentemente dal numero di blocchi try / catch presenti e da dove si trovano.
Giovanni il

1
@BJovke C ++ "gestione delle eccezioni a costo zero" significa solo che non ci sono costi di runtime quando non vengono generate eccezioni, ma c'è ancora un costo di dimensioni del codice maggiore a causa di tutto il codice di pulizia che chiama i distruttori sulle eccezioni. Inoltre, sebbene non vi sia alcun codice specifico dell'eccezione generato nel percorso del codice normale, il costo non è ancora effettivamente zero, poiché la possibilità di eccezioni limita ancora l'ottimizzatore (ad esempio, le cose necessarie in caso di un'eccezione devono rimanere da qualche parte -> i valori possono essere scartati in modo meno aggressivo -> allocazione del registro meno efficiente)
Daniel

24

La struttura è diversa nell'esempio da Ben M . Sarà esteso in testa all'internofor ciclo che non farà un buon confronto tra i due casi.

Quanto segue è più accurato per il confronto in cui l'intero codice da verificare (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 di 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ù rispetto all'altra versione), ma non c'era differenza nella versione di rilascio.

Conclution :
Sulla base di questi prova, penso che possiamo dire che try / catch ha avere un piccolo impatto sulle prestazioni.

EDIT:
ho provato ad aumentare il valore del loop da 10000000 a 1000000000, e ho eseguito di nuovo in versione per ottenere alcune differenze nella versione, 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 non è pertinente. In alcuni casi la versione che usa Try / Catch è in realtà più veloce!


1
Ho notato anche questo, a volte è più veloce con try / catch. L'ho commentato sulla risposta di Ben. Tuttavia, a differenza di 24 elettori, non mi piace questo tipo di benchmarking, non penso che sia una buona indicazione. In questo caso il codice è più veloce, ma lo sarà sempre?
Kobi,

5
Ciò non dimostra che la tua macchina svolgesse contemporaneamente una serie di altre attività? Il tempo trascorso non è mai una buona misura, è necessario utilizzare un profiler che registra il tempo del processore, non il tempo trascorso.
Colin Desmond,

2
@Kobi: sono d'accordo sul fatto che questo non è il modo migliore per eseguire il benchmark se lo pubblicherai come prova che il tuo programma funziona più velocemente di altri o qualcosa del genere, ma può darti come sviluppatore un'indicazione di un metodo che funziona meglio di un altro . In questo caso, penso che possiamo dire che le differenze (almeno per la configurazione di Release) sono ignorabili.
timore del

1
Non stai programmando try/catchqui. Stai programmando 12 tentativi / cattura la sezione di accesso critico contro i loop 10M. Il rumore del loop sradicherà qualsiasi influenza che abbia il try / catch. se invece metti il ​​try / catch all'interno del loop stretto e lo confronti con / senza, finiresti con il costo del try / catch. (senza dubbio, tale codifica non è una buona pratica in generale, ma se si desidera cronometrare il sovraccarico di un costrutto, è così che lo si fa). Oggi BenchmarkDotNet è lo strumento ideale per tempistiche di esecuzione affidabili.
Abel

15

Ho testato l'impatto effettivo di un try..catchcircuito chiuso, ed è troppo piccolo da solo per essere un problema di prestazioni in qualsiasi situazione normale.

Se il ciclo funziona molto poco (nel mio test ne ho fatto uno x++), puoi misurare l'impatto della gestione delle eccezioni. L'esecuzione del ciclo con gestione delle eccezioni ha richiesto circa dieci volte di più.

Se il ciclo funziona davvero (nel mio test ho chiamato il metodo Int32.Parse), la gestione delle eccezioni ha un impatto troppo scarso per essere misurabile. Ho ottenuto una differenza molto più grande scambiando l'ordine dei loop ...


11

provare a catturare i blocchi ha un impatto trascurabile sulle prestazioni ma l'eccezione Il lancio può essere piuttosto considerevole, probabilmente è qui che il tuo collega era confuso.


8

Il tentativo / cattura ha un impatto sulle prestazioni.

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

Ecco un riferimento sulle prestazioni try / catch (non spiega la complessità, ma è implicito). Dai un'occhiata alla sezione Lancia meno eccezioni


3
La complessità è O (1) non significa troppo. Ad esempio se si equipaggia una sezione di codice che viene chiamata molto frequentemente con try-catch (o si menziona un loop), gli O (1) potrebbero aggiungere un numero misurabile alla fine.
Csaba Toth,

6

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 alcune non comuni ma quasi oscure in cui l'effetto può essere evidente. La ragione di ciò è che quel 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 statement2 è garantito per essere eseguito prima di statement3. Se il compilatore è in grado di riconoscere che cosa1 non ha effetti collaterali e cosa2 in realtà non usa x, può tranquillamente omettere del tutto cosa1. Se [come in questo caso] cosa1 fosse costoso, questa potrebbe essere una grande ottimizzazione, anche se i casi in cui cosa1 è costoso sono anche quelli che il compilatore avrebbe meno probabilità di ottimizzare. Supponiamo che il codice sia stato modificato:

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

Ora esiste una sequenza di eventi in cui statement3 potrebbe essere eseguito senza aver eseguito statement2. Anche se nulla nel codice per thing2potrebbe generare un'eccezione, sarebbe possibile che un altro thread potesse usare un Interlocked.CompareExchangeper notare che è qstato cancellato e impostato su Thread.ResetAbort, e quindi eseguire Thread.Abort()un'istruzione before2 in cui ha scritto il suo valore x. Quindi catcheseguirà Thread.ResetAbort()[tramite delegato q], consentendo l'esecuzione per continuare con statement3. Una tale 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, il compilatore ha molte più probabilità di notare opportunità di tralasciare semplici bit di codice rispetto a quelli complessi, e quindi sarebbe raro che un tentativo / cattura possa influire molto sulle prestazioni se non vengono mai generate eccezioni. Tuttavia, ci sono alcune situazioni in cui l'esistenza di un blocco try / catch potrebbe impedire ottimizzazioni che - ma per il tentativo / catch - avrebbero permesso al codice di funzionare più velocemente.


5

Sebbene " Prevenire sia meglio che maneggiare ", nella prospettiva delle prestazioni e dell'efficienza potremmo scegliere il metodo di prova rispetto alla pre-varicazione. Considera il codice seguente:

Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 1; i < int.MaxValue; i++)
{
    if (i != 0)
    {
        int k = 10 / i;
    }
}
stopwatch.Stop();
Console.WriteLine($"With Checking: {stopwatch.ElapsedMilliseconds}");
stopwatch.Reset();
stopwatch.Start();
for (int i = 1; i < int.MaxValue; i++)
{
    try
    {
        int k = 10 / i;
    }
    catch (Exception)
    {

    }
}
stopwatch.Stop();
Console.WriteLine($"With Exception: {stopwatch.ElapsedMilliseconds}");

Ecco il risultato:

With Checking: 20367
With Exception: 13998

4

Vedi 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 altre hanno 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.


Ciò che ho descritto sono due approcci diversi all'implementazione delle eccezioni. Gli approcci si applicano ugualmente a C ++ e C #, nonché al codice gestito / non gestito. Quali MS ha scelto per il loro C # non lo so esattamente, ma l'architettura di gestione delle eccezioni delle applicazioni a livello di macchina fornita da MS utilizza lo schema più veloce. Sarei un po 'sorpreso se l'implementazione C # per 64 bit non la usasse.
Ira Baxter,
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.