¿Por qué es más rápido verificar si el diccionario contiene la clave, en lugar de detectar la excepción en caso de que no lo contenga?

234

Imagina el código:

public class obj
{
    // elided
}

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

Método 1

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

Método 2

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

Tenía curiosidad por saber si hay una diferencia en el rendimiento de estas 2 funciones, porque la primera DEBE ser MÁS LENTA que la segunda, dado que debe verificar dos veces si el diccionario contiene un valor, mientras que la segunda función solo necesita acceder al diccionario una vez pero GUAU, en realidad es lo contrario:

Bucle para 1 000 000 valores (con 100 000 existentes y 900 000 no existentes):

primera función: 306 milisegundos

segunda función: 20483 milisegundos

¿Porqué es eso?

EDITAR: Como puede observar en los comentarios debajo de esta pregunta, el rendimiento de la segunda función es en realidad ligeramente mejor que la primera en caso de que haya 0 teclas no existentes. Pero una vez que hay al menos 1 o más claves no existentes, el rendimiento de la segunda disminuye rápidamente.

Petr
fuente
39
¿Por qué el primero debería ser más lento? En realidad, a primera vista, diría que debería ser más rápido, ContainsKeyse espera O(1)...
Patryk Ćwiek
8
@Petr Hay muchas más instrucciones involucradas en el lanzamiento de excepciones que O(1)en la búsqueda en el diccionario ... Especialmente porque hacer dos O(1)operaciones sigue siendo asintóticamente O(1).
Patryk Ćwiek
99
Como se ha señalado en la buena respuesta a continuación, lanzar excepciones es costoso. Su nombre sugiere esto: están destinados a ser reservados para circunstancias excepcionales . Si está ejecutando un bucle donde consulta un diccionario un millón de veces por claves que no existen, entonces deja de ser una circunstancia excepcional. Si está buscando claves en un diccionario, y es un caso relativamente común que las claves no estarán presentes, entonces tiene sentido verificar primero.
Jason R
66
No olvide que solo ha comparado el costo de verificar un millón de valores ausentes, en lugar de lanzar un millón de excepciones. Pero los dos métodos también difieren en el costo de acceder a un valor existente . Si las claves faltantes son bastante raras, el método de excepción será más rápido en general, a pesar de su mayor costo cuando una clave está ausente.
alexis

Respuestas:

404

Por un lado, lanzar excepciones es inherentemente costoso , porque la pila tiene que ser desenrollada, etc.
Por otro lado, acceder a un valor en un diccionario por su clave es barato, porque es una operación rápida, O (1).

Por cierto: la forma correcta de hacer esto es usar TryGetValue

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

Esto accede al diccionario solo una vez en lugar de dos veces.
Si realmente desea regresar solo nullsi la clave no existe, el código anterior se puede simplificar aún más:

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

Esto funciona, porque se TryGetValueestablece itemen nullsi no nameexiste una clave con .

Daniel Hilgarth
fuente
44
Actualicé mi prueba de acuerdo con la respuesta, y por alguna razón, a pesar de que la función sugerida ES más rápida, en realidad no es muy significativa: 264 ms original, 258ms sugirió uno
Petr
52
@Petr: Sí, no es significativo, porque acceder al diccionario es muy rápido, realmente no importa si lo haces una o dos veces. La mayoría de esos 250 ms probablemente se gastan en el propio bucle de prueba.
Daniel Hilgarth
44
Es bueno saberlo, porque a veces uno tiene la impresión de que lanzar excepciones es una forma mejor o más limpia de manejar una situación como un archivo inexistente o un puntero nulo, independientemente de si esas situaciones son comunes y sin considerar el costo de rendimiento.
LarsH
44
@LarsH también depende de lo que estés haciendo. Si bien los microbenchmarks simples como este muestran sanciones realmente grandes para las excepciones una vez que comienzan sus bucles, incluidas las actividades de archivos o bases de datos, lanzar una excepción en cada iteración es muy poco importante para el rendimiento. Compare la primera y la segunda tabla: codeproject.com/Articles/11265/…
Dan Is Fiddling By Firelight
8
@LarsH También tenga en cuenta que al intentar acceder a un archivo (o algún otro recurso externo), puede cambiar el estado entre la comprobación y el intento de acceso real. En estos casos, usar excepciones es la forma correcta de hacerlo. Consulte la respuesta de Stephen C a esta pregunta para obtener información adicional.
yoniLavi
6

Los diccionarios están diseñados específicamente para realizar búsquedas de teclas súper rápidas. Se implementan como tablas hash y cuantas más entradas, más rápido son en relación con otros métodos. Se supone que el uso del motor de excepción solo se debe hacer cuando su método no ha podido hacer lo que usted diseñó porque es un gran conjunto de objetos que le brinda mucha funcionalidad para manejar errores. ¡Construí una clase de biblioteca completa una vez con todo rodeado de bloques de prueba una vez y me horroricé al ver la salida de depuración que contenía una línea separada para cada una de las más de 600 excepciones!

Ed Hermanson
fuente
1
Cuando los implementadores de lenguaje deciden dónde gastar esfuerzos en la optimización, las tablas hash tendrán prioridad porque se usan con frecuencia, a menudo en bucles internos que pueden ser cuellos de botella. Se espera que las excepciones se usen con mucha menos frecuencia, en casos inusuales ("excepcionales", por así decirlo), por lo que generalmente no se consideran importantes para el rendimiento.
Barmar
"Se implementan como tablas hash y cuantas más entradas, más rápido son en relación con otros métodos". seguramente eso no es cierto si los cubos se llenan?!?!
AnthonyLambert
1
@AnthonyLambert Lo que está tratando de decir es que buscar en una tabla hash tiene O (1) complejidad de tiempo, mientras que una búsqueda de árbol de búsqueda binaria tendría O (log (n)); el árbol se ralentiza a medida que aumenta el número de elementos de forma asintótica, mientras que la tabla hash no. Por lo tanto, la ventaja de velocidad de la tabla hash aumenta con el número de elementos, aunque lo hace lentamente.
Doval
@AnthonyLambert Bajo uso normal, hay muy pocas colisiones en la tabla hash de un Diccionario. Si está usando una tabla hash y sus cubos se llenan, tiene muuuuchas demasiadas entradas (o muy pocos cubos). En ese caso, es hora de usar una tabla hash personalizada.
AndrewS