Perché è più veloce verificare se il dizionario contiene la chiave, piuttosto che catturare l'eccezione nel caso non lo fosse?


234

Immagina il codice:

public class obj
{
    // elided
}

public static Dictionary<string, obj> dict = new Dictionary<string, obj>();

Metodo 1

public static obj FromDict1(string name)
{
    if (dict.ContainsKey(name))
    {
        return dict[name];
    }
    return null;
}

Metodo 2

public static obj FromDict2(string name)
{
    try
    {
        return dict[name];
    }
    catch (KeyNotFoundException)
    {
        return null;
    }
}

Ero curioso di sapere se c'è una differenza nelle prestazioni di queste 2 funzioni, perché la prima DOVREBBE ESSERE PIÙ LENTA della seconda - dato che deve controllare due volte se il dizionario contiene un valore, mentre la seconda funzione deve accedere solo al dizionario una volta ma WOW, in realtà è l'opposto:

Ciclo per 1 000 000 valori (con 100000 esistenti e 900000 inesistenti):

prima funzione: 306 millisecondi

seconda funzione: 20483 millisecondi

Perché?

EDIT: Come puoi notare nei commenti sotto questa domanda, l'esecuzione della seconda funzione è in realtà leggermente migliore della prima nel caso in cui ci siano 0 chiavi non esistenti. Ma una volta che ci sono almeno 1 o più chiavi inesistenti, le prestazioni della seconda diminuiscono rapidamente.


39
Perché il primo dovrebbe essere più lento? In realtà, a prima vista, direi che dovrebbe essere più veloce, ContainsKeyè previsto O(1)...
Patryk Ćwiek,


8
@Petr Ci sono molte più istruzioni coinvolte nel lancio dell'eccezione che la O(1)ricerca nel dizionario ... Soprattutto dal momento che fare due O(1)operazioni è ancora asintoticamente O(1).
Patryk Ćwiek,

9
Come è stato notato nella buona risposta di seguito, generare eccezioni è costoso. Il loro nome suggerisce questo: sono pensati per essere riservati a circostanze eccezionali . Se stai eseguendo un ciclo in cui esegui query su un dizionario un milione di volte per chiavi che non esistono, allora cessa di essere una circostanza eccezionale. Se stai interrogando un dizionario per le chiavi, ed è un caso relativamente comune che le chiavi non siano presenti, allora ha senso controllare prima.
Jason R,

6
Non dimenticare che hai solo confrontato il costo della verifica di un milione di valori assenti, rispetto al lancio di un milione di eccezioni. Ma i due metodi differiscono anche nel costo di accesso a un valore esistente . Se le chiavi mancanti sono abbastanza rare, il metodo dell'eccezione sarà più veloce di tutto, nonostante il suo costo maggiore quando una chiave è assente.
alexis

Risposte:


404

Da una parte, lanciare eccezioni è intrinsecamente costoso , perché lo stack deve essere srotolato, ecc.
D'altra parte, accedere a un valore in un dizionario con la sua chiave è economico, perché è un'operazione O (1) veloce.

A proposito: il modo corretto per farlo è usare TryGetValue

obj item;
if(!dict.TryGetValue(name, out item))
    return null;
return item;

Questo accede al dizionario solo una volta anziché due volte.
Se vuoi davvero tornare solo nullse la chiave non esiste, il codice sopra può essere ulteriormente semplificato:

obj item;
dict.TryGetValue(name, out item);
return item;

Funziona, perché TryGetValueimpostato itemsu nullse non nameesiste alcuna chiave .


4
Ho aggiornato il mio test in base alla risposta, e per qualche ragione, nonostante la funzione suggerita sia più veloce, in realtà non è molto significativa: 264 ms originale, 258 ms suggerito uno
Petr

52
@Petr: Sì, non è significativo, perché l'accesso al dizionario è molto veloce, non importa se lo fai una o due volte. La maggior parte di quei 250 ms è probabilmente spesa nel loop di test stesso.
Daniel Hilgarth,

4
Questo è buono a sapersi, perché a volte si ha l'impressione che il lancio di eccezioni sia un modo migliore o più pulito per gestire una situazione come file inesistente o puntatore nullo, indipendentemente dal fatto che tali situazioni siano comuni e senza considerare il costo delle prestazioni.
LarsH

4
@LarsH dipende anche da cosa stai facendo. Mentre microbenchmark semplici come questo mostrano penalità molto elevate per le eccezioni una volta che i loop iniziano includendo attività di file o database che generano un'eccezione su ogni iterazione conta molto poco per le prestazioni. Confronta 1 ° e 2 ° tavolo: codeproject.com/Articles/11265/…
Dan sta giocherellando alla luce del fuoco

8
@LarsH Si noti inoltre che quando si tenta di accedere a un file (o qualche altra risorsa esterna), potrebbe cambiare stato tra il controllo e il tentativo di accesso effettivo. In questi casi, l'uso delle eccezioni è il modo corretto di procedere. Vedi la risposta di Stephen C a questa domanda per ulteriori approfondimenti.
yoniLavi,

6

I dizionari sono progettati specificamente per eseguire ricerche di tasti super veloci. Sono implementati come hashtabili e più voci sono più veloci sono rispetto ad altri metodi. L'uso del motore delle eccezioni dovrebbe essere fatto solo quando il metodo non è riuscito a fare ciò per cui è stato progettato perché è un grande insieme di oggetti che offre molte funzionalità per la gestione degli errori. Ho creato un'intera classe di libreria una volta con tutto circondato da try catch blocks una volta e sono rimasto sconvolto nel vedere l'output di debug che conteneva una linea separata per ciascuna delle oltre 600 eccezioni!


1
Quando gli implementatori del linguaggio decidono dove investire gli sforzi per l'ottimizzazione, le tabelle hash avranno la priorità perché vengono utilizzate frequentemente, spesso in cicli interni che possono essere colli di bottiglia. Si prevede che le eccezioni vengano utilizzate solo molto meno frequentemente, in casi insoliti ("eccezionali", per così dire), quindi di solito non sono considerate importanti per le prestazioni.
Barmar,

"Sono implementati come hashtabili e più voci più velocemente sono rispetto ad altri metodi." sicuramente questo non è vero se i secchi si riempiono?!?!
AnthonyLambert,

1
@AnthonyLambert Quello che sta cercando di dire è che la ricerca di una hashtable ha una complessità temporale O (1), mentre una ricerca dell'albero di ricerca binaria avrebbe O (log (n)); l'albero rallenta man mano che il numero di elementi aumenta in modo asintotico, mentre l'hashtable no. Pertanto, il vantaggio di velocità dell'hashtable aumenta con il numero di elementi, sebbene lo faccia lentamente.
Doval,

@AnthonyLambert Nell'uso normale, ci sono pochissime collisioni nella tabella di hash di un dizionario. Se stai usando una tabella hash e i tuoi secchi si riempiono, hai troppe voci (o troppi secchi). In tal caso, è tempo di utilizzare una tabella hash personalizzata.
AndrewS
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.