Pourquoi est-il plus rapide de vérifier si le dictionnaire contient la clé, plutôt que d'attraper l'exception au cas où elle ne le ferait pas?

234

Imaginez le code:

public class obj
{
    // elided
}

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

Méthode 1

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

Méthode 2

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

J'étais curieux de savoir s'il y avait une différence dans les performances de ces 2 fonctions, car la première DEVRAIT être plus LENTE que la seconde - étant donné qu'elle doit vérifier deux fois si le dictionnaire contient une valeur, tandis que la deuxième fonction n'a besoin d'accéder qu'au dictionnaire uniquement une fois mais WOW, c'est en fait l'opposé:

Boucle pour 1 000 000 de valeurs (avec 100 000 existantes et 900 000 non existantes):

première fonction: 306 millisecondes

deuxième fonction: 20483 millisecondes

Pourquoi donc?

EDIT: Comme vous pouvez le remarquer dans les commentaires ci-dessous cette question, les performances de la deuxième fonction sont en fait légèrement meilleures que la première dans le cas où il n'y a 0 clé non existante. Mais une fois qu'il y a au moins 1 ou plusieurs clés non existantes, les performances de la seconde diminuent rapidement.

Petr
la source
39
Pourquoi le premier devrait être plus lent? En fait, à première vue, je dirais que ça devrait être plus rapide, ContainsKeyc'est prévu O(1)...
Patryk Ćwiek
8
@Petr Il y a beaucoup plus d'instructions impliquées dans le lancement d'exceptions que de O(1)recherche dans le dictionnaire ... D'autant plus que faire deux O(1)opérations est toujours asymptotique O(1).
Patryk Ćwiek
9
Comme cela a été noté dans la bonne réponse ci-dessous, lever des exceptions coûte cher. Leur nom le suggère: ils sont destinés à être réservés à des circonstances exceptionnelles . Si vous exécutez une boucle où vous interrogez un dictionnaire un million de fois pour des clés qui n'existent pas, cela cesse en quelque sorte d'être une circonstance exceptionnelle. Si vous interrogez un dictionnaire pour des clés, et qu'il est relativement courant qu'elles ne soient pas présentes, il est logique de vérifier d'abord.
Jason R
6
N'oubliez pas que vous n'avez comparé que le coût de la vérification d'un million de valeurs absentes, par rapport à la levée d'un million d'exceptions. Mais les deux méthodes diffèrent également par le coût d'accès à une valeur existante . Si les clés manquantes sont assez rares, la méthode d'exception sera plus rapide dans l'ensemble, malgré son coût plus élevé lorsqu'une clé est absente.
alexis

Réponses:

404

D'une part, lever des exceptions est intrinsèquement coûteux , car la pile doit être déroulée, etc.
D'un autre côté, accéder à une valeur dans un dictionnaire par sa clé est bon marché, car il s'agit d'une opération O (1) rapide.

BTW: La bonne façon de procéder consiste à utiliser TryGetValue

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

Cela accède au dictionnaire une seule fois au lieu de deux.
Si vous voulez vraiment retourner nullsi la clé n'existe pas, le code ci-dessus peut être simplifié davantage:

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

Cela fonctionne, parce que les TryGetValueensembles itemà nullsi aucune clé avec nameexiste.

Daniel Hilgarth
la source
4
J'ai mis à jour mon test en fonction de la réponse, et pour une raison quelconque, malgré la fonction suggérée EST plus rapide, ce n'est en fait pas très important: 264 ms d'origine, 258 ms suggéré
Petr
52
@Petr: Oui, ce n'est pas significatif, car l'accès au dictionnaire est très rapide, peu importe que vous le fassiez une ou deux fois. La plupart de ces 250 ms sont probablement dépensés dans la boucle de test elle-même.
Daniel Hilgarth
4
C'est bon à savoir, car parfois on a l'impression que le lancement d'exceptions est un moyen meilleur ou plus propre de gérer une situation comme un fichier inexistant ou un pointeur nul, que ces situations soient courantes et sans tenir compte du coût des performances.
LarsH
4
@LarsH cela dépend aussi de ce que vous faites. Alors que de simples microbenchmarks comme celui-ci entraînent de très lourdes pénalités pour les exceptions une fois que vos boucles commencent, y compris les activités de fichier ou de base de données, lever une exception à chaque itération importe très peu pour les performances. Comparez les 1er et 2e tableaux: codeproject.com/Articles/11265/…
Dan est en train de tripoter par Firelight le
8
@LarsH Notez également que lorsque vous essayez d'accéder à un fichier (ou à une autre ressource externe), il peut changer d'état entre la vérification et la tentative d'accès réelle. Dans ces cas, l'utilisation d'exceptions est la bonne façon de procéder. Voir la réponse de Stephen C à cette question pour plus d'informations.
yoniLavi
6

Les dictionnaires sont spécialement conçus pour effectuer des recherches de touches ultra rapides. Ils sont implémentés sous forme de tables de hachage et plus il y a d'entrées, plus ils sont rapides par rapport aux autres méthodes. L'utilisation du moteur d'exception n'est censée être effectuée que lorsque votre méthode n'a pas réussi à faire ce que vous avez conçu, car il s'agit d'un grand ensemble d'objets qui vous offrent de nombreuses fonctionnalités pour gérer les erreurs. J'ai construit une classe de bibliothèque entière une fois avec tout entouré de blocs catch catch une fois et j'ai été consterné de voir la sortie de débogage qui contenait une ligne distincte pour chacune des 600 exceptions!

Ed Hermanson
la source
1
Lorsque les implémenteurs de langage décident où déployer des efforts d'optimisation, les tables de hachage auront la priorité car elles sont utilisées fréquemment, souvent dans des boucles internes qui peuvent être des goulots d'étranglement. Les exceptions ne devraient être utilisées que beaucoup moins fréquemment, dans des cas inhabituels ("exceptionnels", pour ainsi dire), de sorte qu'elles ne sont généralement pas considérées comme importantes pour les performances.
Barmar
"Ils sont implémentés sous forme de tables de hachage et plus il y a d'entrées, plus ils sont rapides par rapport aux autres méthodes." ce n'est sûrement pas vrai si les seaux se remplissent?!?!
AnthonyLambert
1
@AnthonyLambert Ce qu'il essaie de dire, c'est que la recherche d'une table de hachage a une complexité temporelle de O (1), alors qu'une recherche d'arbre de recherche binaire aurait O (log (n)); l'arborescence ralentit lorsque le nombre d'éléments augmente asymptotiquement, contrairement à la table de hachage. Par conséquent, l'avantage de vitesse de la table de hachage augmente avec le nombre d'éléments, bien qu'elle le fasse lentement.
Doval
@AnthonyLambert Dans des conditions normales d'utilisation, il y a très peu de collisions dans la table de hachage d'un dictionnaire. Si vous utilisez une table de hachage et que vos compartiments se remplissent, vous avez trop d'entrées waaaaay (ou trop peu de compartiments). Dans ce cas, il est temps d'utiliser une table de hachage personnalisée.
AndrewS