¿Qué es más eficiente: Dictionary TryGetValue o ContainsKey + Item?

252

De la entrada de MSDN en Dictionary. TryGetValue Method :

Este método combina la funcionalidad del método ContainsKey y la propiedad Item.

Si no se encuentra la clave, entonces el parámetro de valor obtiene el valor predeterminado apropiado para el tipo de valor TValue; por ejemplo, 0 (cero) para tipos enteros, falso para tipos booleanos y nulo para tipos de referencia.

Use el método TryGetValue si su código intenta acceder con frecuencia a claves que no están en el diccionario. Usar este método es más eficiente que capturar la KeyNotFoundException generada por la propiedad Item.

Este método se acerca a una operación O (1).

De la descripción, no está claro si es más eficiente o simplemente más conveniente que llamar a ContainsKey y luego realizar la búsqueda. ¿La implementación de TryGetValuesimplemente llama a ContainsKey y luego a Item o es realmente más eficiente que eso al hacer una sola búsqueda?

En otras palabras, qué es más eficiente (es decir, cuál realiza menos búsquedas):

Dictionary<int,int> dict;
//...//
int ival;
if(dict.ContainsKey(ikey))
{
  ival = dict[ikey];
}
else
{
  ival = default(int);
}

o

Dictionary<int,int> dict;
//...//
int ival;
dict.TryGetValue(ikey, out ival);

Nota: ¡No estoy buscando un punto de referencia!

Rado
fuente

Respuestas:

314

TryGetValue Será más rápido.

ContainsKeyusa la misma verificación que TryGetValue, que internamente se refiere a la ubicación de entrada real. La Itempropiedad en realidad tiene una funcionalidad de código casi idéntica TryGetValue, excepto que arrojará una excepción en lugar de devolver false.

El uso ContainsKeyseguido por el Itembásicamente duplica la funcionalidad de búsqueda, que es la mayor parte del cálculo en este caso.

Reed Copsey
fuente
2
Esto es más sutil: if(dict.ContainsKey(ikey)) dict[ikey]++; else dict.Add(ikey, 0);. Pero creo que eso TryGetValuees aún más eficiente ya que se usa el get y set de la propiedad indexadora, ¿no es así?
Tim Schmelter
44
ahora también puede ver la fuente .net para ella: referencesource.microsoft.com/#mscorlib/system/collections/… puede ver que los 3 TryGetValue, ContainsKey, y esto [] llaman al mismo método FindEntry y hacen la misma cantidad de trabajo, solo difiere en la forma en que responden la pregunta: trygetvalue devuelve bool y el valor, contiene key solo devuelve verdadero / falso, y esto [] devuelve el valor o arroja una excepción.
John Gardner
1
@JohnGardner Sí, que es lo que dije, pero si haces ContainsKey y obtienes Item, estás haciendo ese trabajo 2x en lugar de 1x.
Reed Copsey
3
Estoy completamente de acuerdo :) Estaba señalando que la fuente real está disponible ahora. ninguna de las otras respuestas / etc. tenía un enlace a la fuente real: D
John Gardner
1
Un poco fuera de tema, si está accediendo a través de un IDictionary en un entorno multiproceso, siempre usaría TryGetValue ya que el estado puede cambiar desde el momento en que llama a ContainsKey (tampoco hay garantía de que TryGetValue se bloquee internamente correctamente, pero probablemente sea más seguro)
Chris Berry
91

Un punto de referencia rápido muestra que TryGetValuetiene una ligera ventaja:

    static void Main() {
        var d = new Dictionary<string, string> {{"a", "b"}};
        var start = DateTime.Now;
        for (int i = 0; i != 10000000; i++) {
            string x;
            if (!d.TryGetValue("a", out x)) throw new ApplicationException("Oops");
            if (d.TryGetValue("b", out x)) throw new ApplicationException("Oops");
        }
        Console.WriteLine(DateTime.Now-start);
        start = DateTime.Now;
        for (int i = 0; i != 10000000; i++) {
            string x;
            if (d.ContainsKey("a")) {
                x = d["a"];
            } else {
                x = default(string);
            }
            if (d.ContainsKey("b")) {
                x = d["b"];
            } else {
                x = default(string);
            }
        }
   }

Esto produce

00:00:00.7600000
00:00:01.0610000

haciendo el ContainsKey + Itemacceso un 40% más lento suponiendo una combinación uniforme de aciertos y errores.

Además, cuando cambio el programa para que siempre pierda (es decir, siempre busco "b") las dos versiones se vuelven igualmente rápidas:

00:00:00.2850000
00:00:00.2720000

Sin embargo, cuando lo hago "todos los éxitos", TryGetValuesigue siendo un claro ganador:

00:00:00.4930000
00:00:00.8110000
dasblinkenlight
fuente
11
Por supuesto, depende del patrón de uso real. Si casi nunca falla una búsqueda, entonces TryGetValuedebe estar muy por delante. Además ... un punto crítico ... DateTimeno es la mejor manera de capturar mediciones de rendimiento.
Ed S.
44
@EdS. Tienes razón, se TryGetValuepone aún más en la delantera. Edité la respuesta para incluir los escenarios "todos los aciertos" y "todos los errores".
dasblinkenlight
2
@Luciano explicar cómo se utiliza Any- De esta manera: Any(i=>i.Key==key). En cuyo caso, sí, esa es una mala búsqueda lineal del diccionario.
weston
13
DateTime.Nowsolo será preciso a unos pocos ms. Utilice la Stopwatchclase en su System.Diagnosticslugar (que utiliza QueryPerformanceCounter debajo de las cubiertas para proporcionar una precisión mucho mayor). También es más fácil de usar.
Alastair Maw
55
Además de los comentarios de Alastair y Ed: DateTime. Now puede ir hacia atrás, si obtiene una actualización de hora, como la que ocurre cuando el usuario actualiza la hora de su computadora, se cruza una zona horaria o la zona horaria cambia (DST, por ejemplo). Intente trabajar en un sistema que tenga el reloj del sistema sincronizado a la hora proporcionada por algún servicio de radio como GPS o redes de telefonía móvil. DateTime.Now irá por todas partes, y DateTime.UtcNow solo corrige una de esas causas. Solo usa StopWatch.
antiduh
51

Como ninguna de las respuestas hasta ahora responde realmente a la pregunta, aquí hay una respuesta aceptable que encontré después de algunas investigaciones:

Si descompila TryGetValue, verá que está haciendo esto:

public bool TryGetValue(TKey key, out TValue value)
{
  int index = this.FindEntry(key);
  if (index >= 0)
  {
    value = this.entries[index].value;
    return true;
  }
  value = default(TValue);
  return false;
}

mientras que el método ContainsKey es:

public bool ContainsKey(TKey key)
{
  return (this.FindEntry(key) >= 0);
}

entonces TryGetValue es solo ContainsKey más una búsqueda de matriz si el elemento está presente.

Fuente

Parece que TryGetValue será casi el doble de rápido que la combinación ContainsKey + Item.

Rado
fuente
20

A quien le importa :-)

Probablemente esté preguntando porque TryGetValuees difícil de usar, así que encapsúlelo así con un método de extensión.

public static class CollectionUtils
{
    // my original method
    // public static V GetValueOrDefault<K, V>(this Dictionary<K, V> dic, K key)
    // {
    //    V ret;
    //    bool found = dic.TryGetValue(key, out ret);
    //    if (found)
    //    {
    //        return ret;
    //    }
    //    return default(V);
    // }


    // EDIT: one of many possible improved versions
    public static TValue GetValueOrDefault<K, V>(this IDictionary<K, V> dictionary, K key)
    {
        // initialized to default value (such as 0 or null depending upon type of TValue)
        TValue value;  

        // attempt to get the value of the key from the dictionary
        dictionary.TryGetValue(key, out value);
        return value;
    }

Entonces solo llame:

dict.GetValueOrDefault("keyname")

o

(dict.GetValueOrDefault("keyname") ?? fallbackValue) 
Simon_Weaver
fuente
1
@ Hüseyin Me confundí mucho porque era lo suficientemente estúpido como para publicar esto sin, thispero resulta que tengo mi método duplicado dos veces en mi base de código, una vez con y una sin la, ¡ thispor eso nunca lo entendí! gracias por arreglar!
Simon_Weaver
2
TryGetValue asigna un valor predeterminado al parámetro de valor de salida si la clave no existe, por lo que esto podría simplificarse.
Raphael Smit
2
Versión simplificada: public static TValue GetValueOrDefault <TKey, TValue> (este diccionario <TKey, TValue> dict, tecla TKey) {TValue ret; dict. TryGetValue (clave, fuera ret); volver ret; }
Joshua
2
En C # 7 esto es realmente divertido:if(!dic.TryGetValue(key, out value item)) item = dic[key] = new Item();
Shimmy Weitzhandler
1
Irónicamente, el código fuente real ya TIENE una rutina GetValueOrDefault (), pero está oculto ... referencesource.microsoft.com/#mscorlib/system/collections/…
Deven T. Corzine
10

¿Por qué no lo pruebas?

Pero estoy bastante seguro de que TryGetValue es más rápido, porque solo hace una búsqueda. Por supuesto, esto no está garantizado, es decir, diferentes implementaciones pueden tener diferentes características de rendimiento.

La forma en que implementaría un diccionario es creando una Findfunción interna que encuentre el espacio para un elemento, y luego construya el resto sobre eso.

CodesInChaos
fuente
No creo que los detalles de implementación puedan cambiar la garantía de que hacer la acción X una vez es más rápido o igual que hacer la acción X dos veces. En el mejor de los casos, son idénticos, en el peor de los casos, la versión 2X tarda el doble.
Dan Bechard
9

Todas las respuestas hasta ahora, aunque buenas, pierden un punto vital.

Los métodos en las clases de una API (por ejemplo, el marco .NET) forman parte de una definición de interfaz (no una interfaz C # o VB, sino una interfaz en el significado de la informática).

Como tal, generalmente es incorrecto preguntar si llamar a un método de este tipo es más rápido, a menos que la velocidad sea parte de la definición formal de la interfaz (que no es en este caso).

Tradicionalmente, este tipo de acceso directo (que combina búsqueda y recuperación) es más eficiente independientemente del idioma, la infraestructura, el sistema operativo, la plataforma o la arquitectura de la máquina. También es más legible, porque expresa su intención explícitamente, en lugar de implicarla (desde la estructura de su código).

Entonces, la respuesta (de un viejo truco canoso) es definitivamente 'Sí' (TryGetValue es preferible a una combinación de ContainsKey y Item [Get] para recuperar un valor de un Diccionario).

Si cree que esto suena extraño, piense así: incluso si las implementaciones actuales de TryGetValue, ContainsKey y Item [Get] no producen ninguna diferencia de velocidad, puede suponer que es probable que una implementación futura (por ejemplo .NET v5) hará (TryGetValue será más rápido). Piensa en la vida útil de tu software.

Por otro lado, es interesante observar que las tecnologías de definición de interfaz modernas típicas todavía rara vez proporcionan algún medio para definir formalmente las restricciones de tiempo. ¿Quizás .NET v5?

polemista
fuente
2
Si bien estoy 100% de acuerdo con su argumento sobre la semántica, todavía vale la pena hacer la prueba de rendimiento. Nunca se sabe cuando la API que está utilizando tiene una implementación subóptima, de modo que lo semánticamente correcto es más lento, a menos que haga la prueba.
Dan Bechard
5

Al hacer un programa de prueba rápida, definitivamente hay una mejora usando TryGetValue con 1 millón de elementos en un diccionario.

Resultados:

Contiene Key + Item para 1000000 golpes: 45ms

TryGetValue para 1000000 visitas: 26 ms

Aquí está la aplicación de prueba:

static void Main(string[] args)
{
    const int size = 1000000;

    var dict = new Dictionary<int, string>();

    for (int i = 0; i < size; i++)
    {
        dict.Add(i, i.ToString());
    }

    var sw = new Stopwatch();
    string result;

    sw.Start();

    for (int i = 0; i < size; i++)
    {
        if (dict.ContainsKey(i))
            result = dict[i];
    }

    sw.Stop();
    Console.WriteLine("ContainsKey + Item for {0} hits: {1}ms", size, sw.ElapsedMilliseconds);

    sw.Reset();
    sw.Start();

    for (int i = 0; i < size; i++)
    {
        dict.TryGetValue(i, out result);
    }

    sw.Stop();
    Console.WriteLine("TryGetValue for {0} hits: {1}ms", size, sw.ElapsedMilliseconds);

}
davisoa
fuente
5

En mi máquina, con mucha RAM, cuando se ejecuta en modo RELEASE (no DEBUG), ContainsKeyes igual TryGetValue/ try-catchsi todas las entradas en elDictionary<> .

ContainsKeylos supera a todos con diferencia cuando solo se encuentran algunas entradas del diccionario (en mi ejemplo a continuación, establezca MAXVALalgo más grande que ENTRIESperder algunas entradas):

Resultados:

Finished evaluation .... Time distribution:
Size: 000010: TryGetValue: 53,24%, ContainsKey: 1,74%, try-catch: 45,01% - Total: 2.006,00
Size: 000020: TryGetValue: 37,66%, ContainsKey: 0,53%, try-catch: 61,81% - Total: 2.443,00
Size: 000040: TryGetValue: 22,02%, ContainsKey: 0,73%, try-catch: 77,25% - Total: 7.147,00
Size: 000080: TryGetValue: 31,46%, ContainsKey: 0,42%, try-catch: 68,12% - Total: 17.793,00
Size: 000160: TryGetValue: 33,66%, ContainsKey: 0,37%, try-catch: 65,97% - Total: 36.840,00
Size: 000320: TryGetValue: 34,53%, ContainsKey: 0,39%, try-catch: 65,09% - Total: 71.059,00
Size: 000640: TryGetValue: 32,91%, ContainsKey: 0,32%, try-catch: 66,77% - Total: 141.789,00
Size: 001280: TryGetValue: 39,02%, ContainsKey: 0,35%, try-catch: 60,64% - Total: 244.657,00
Size: 002560: TryGetValue: 35,48%, ContainsKey: 0,19%, try-catch: 64,33% - Total: 420.121,00
Size: 005120: TryGetValue: 43,41%, ContainsKey: 0,24%, try-catch: 56,34% - Total: 625.969,00
Size: 010240: TryGetValue: 29,64%, ContainsKey: 0,61%, try-catch: 69,75% - Total: 1.197.242,00
Size: 020480: TryGetValue: 35,14%, ContainsKey: 0,53%, try-catch: 64,33% - Total: 2.405.821,00
Size: 040960: TryGetValue: 37,28%, ContainsKey: 0,24%, try-catch: 62,48% - Total: 4.200.839,00
Size: 081920: TryGetValue: 29,68%, ContainsKey: 0,54%, try-catch: 69,77% - Total: 8.980.230,00

Aquí está mi código:

    using System;
    using System.Collections.Generic;
    using System.Diagnostics;

    namespace ConsoleApplication1
    {
        class Program
        {
            static void Main(string[] args)
            {
                const int ENTRIES = 10000, MAXVAL = 15000, TRIALS = 100000, MULTIPLIER = 2;
                Dictionary<int, int> values = new Dictionary<int, int>();
                Random r = new Random();
                int[] lookups = new int[TRIALS];
                int val;
                List<Tuple<long, long, long>> durations = new List<Tuple<long, long, long>>(8);

                for (int i = 0;i < ENTRIES;++i) try
                    {
                        values.Add(r.Next(MAXVAL), r.Next());
                    }
                    catch { --i; }

                for (int i = 0;i < TRIALS;++i) lookups[i] = r.Next(MAXVAL);

                Stopwatch sw = new Stopwatch();
                ConsoleColor bu = Console.ForegroundColor;

                for (int size = 10;size <= TRIALS;size *= MULTIPLIER)
                {
                    long a, b, c;

                    Console.ForegroundColor = ConsoleColor.Yellow;
                    Console.WriteLine("Loop size: {0}", size);
                    Console.ForegroundColor = bu;

                    // ---------------------------------------------------------------------
                    sw.Start();
                    for (int i = 0;i < size;++i) values.TryGetValue(lookups[i], out val);
                    sw.Stop();
                    Console.WriteLine("TryGetValue: {0}", a = sw.ElapsedTicks);

                    // ---------------------------------------------------------------------
                    sw.Restart();
                    for (int i = 0;i < size;++i) val = values.ContainsKey(lookups[i]) ? values[lookups[i]] : default(int);
                    sw.Stop();
                    Console.WriteLine("ContainsKey: {0}", b = sw.ElapsedTicks);

                    // ---------------------------------------------------------------------
                    sw.Restart();
                    for (int i = 0;i < size;++i)
                        try { val = values[lookups[i]]; }
                        catch { }
                    sw.Stop();
                    Console.WriteLine("try-catch: {0}", c = sw.ElapsedTicks);

                    // ---------------------------------------------------------------------
                    Console.WriteLine();

                    durations.Add(new Tuple<long, long, long>(a, b, c));
                }

                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.WriteLine("Finished evaluation .... Time distribution:");
                Console.ForegroundColor = bu;

                val = 10;
                foreach (Tuple<long, long, long> d in durations)
                {
                    long sum = d.Item1 + d.Item2 + d.Item3;

                    Console.WriteLine("Size: {0:D6}:", val);
                    Console.WriteLine("TryGetValue: {0:P2}, ContainsKey: {1:P2}, try-catch: {2:P2} - Total: {3:N}", (decimal)d.Item1 / sum, (decimal)d.Item2 / sum, (decimal)d.Item3 / sum, sum);
                    val *= MULTIPLIER;
                }

                Console.WriteLine();
            }
        }
    }
AxD
fuente
Siento que algo sospechoso está sucediendo aquí. Me pregunto si el optimizador podría estar eliminando o simplificando sus comprobaciones ContainsKey () debido al hecho de que nunca utiliza el valor recuperado.
Dan Bechard
Simplemente no puede. ContainsKey () está en una DLL compilada. El optimizador no sabe nada sobre lo que realmente hace ContainsKey (). Puede causar efectos secundarios, por lo que debe llamarse y no puede resumirse.
AxD
Algo es falso aquí. El hecho es que examinar el código .NET muestra que ContainsKey, TryGetValue, y esto [] todos llaman al mismo código interno, por lo que TryGetValue es más rápido que ContainsKey + this [] cuando existe la entrada.
Jim Balter
3

Además de diseñar un microbenchmark que brinde resultados precisos en un entorno práctico, puede inspeccionar la fuente de referencia de .NET Framework.

Todos ellos llaman al FindEntry(TKey)método que hace la mayor parte del trabajo y no memoriza su resultado, por lo que llamar TryGetValuees casi el doble de rápido que ContainsKey+Item .


La interfaz inconveniente de TryGetValuese puede adaptar usando un método de extensión :

using System.Collections.Generic;

namespace Project.Common.Extensions
{
    public static class DictionaryExtensions
    {
        public static TValue GetValueOrDefault<TKey, TValue>(
            this IDictionary<TKey, TValue> dictionary,
            TKey key,
            TValue defaultValue = default(TValue))
        {
            if (dictionary.TryGetValue(key, out TValue value))
            {
                return value;
            }
            return defaultValue;
        }
    }
}

Desde C # 7.1, puede reemplazar default(TValue)con plain default. El tipo se infiere.

Uso:

var dict = new Dictionary<string, string>();
string val = dict.GetValueOrDefault("theKey", "value used if theKey is not found in dict");

Devuelve los nulltipos de referencia cuya búsqueda falla, a menos que se especifique un valor predeterminado explícito.

var dictObj = new Dictionary<string, object>();
object valObj = dictObj.GetValueOrDefault("nonexistent");
Debug.Assert(valObj == null);

val dictInt = new Dictionary<string, int>();
int valInt = dictInt.GetValueOrDefault("nonexistent");
Debug.Assert(valInt == 0);
Palec
fuente
Tenga en cuenta que los usuarios del método de extensión no pueden distinguir entre una clave inexistente y una clave que existe, pero su valor es predeterminado (T).
Lucas
En una computadora moderna, si llama a una subrutina dos veces seguidas, es poco probable que tome el doble de tiempo que llamarla una vez. Esto se debe a que es muy probable que la arquitectura de CPU y almacenamiento en caché almacene en caché muchas de las instrucciones y datos asociados con la primera llamada, por lo que la segunda llamada se ejecutará más rápido. Por otro lado, es casi seguro que llamar dos veces tomará un poco más de tiempo que llamar una vez, por lo que aún es una ventaja eliminar la segunda llamada si es posible.
debate el
2

Si está tratando de obtener el valor del diccionario, TryGetValue (clave, valor de salida) es la mejor opción, pero si está buscando la presencia de la clave, para una nueva inserción, sin sobrescribir las claves antiguas, y solo con ese alcance, ContainsKey (clave) es la mejor opción, el punto de referencia puede confirmar esto:

using System;
using System.Threading;
using System.Diagnostics;
using System.Collections.Generic;
using System.Collections;

namespace benchmark
{
class Program
{
    public static Random m_Rand = new Random();
    public static Dictionary<int, int> testdict = new Dictionary<int, int>();
    public static Hashtable testhash = new Hashtable();

    public static void Main(string[] args)
    {
        Console.WriteLine("Adding elements into hashtable...");
        Stopwatch watch = Stopwatch.StartNew();
        for(int i=0; i<1000000; i++)
            testhash[i]=m_Rand.Next();
        watch.Stop();
        Console.WriteLine("Done in {0:F4} -- pause....", watch.Elapsed.TotalSeconds);
        Thread.Sleep(4000);
        Console.WriteLine("Adding elements into dictionary...");
        watch = Stopwatch.StartNew();
        for(int i=0; i<1000000; i++)
            testdict[i]=m_Rand.Next();
        watch.Stop();
        Console.WriteLine("Done in {0:F4} -- pause....", watch.Elapsed.TotalSeconds);
        Thread.Sleep(4000);

        Console.WriteLine("Finding the first free number for insertion");
        Console.WriteLine("First method: ContainsKey");
        watch = Stopwatch.StartNew();
        int intero=0;
        while (testdict.ContainsKey(intero))
        {
            intero++;
        }
        testdict.Add(intero, m_Rand.Next());
        watch.Stop();
        Console.WriteLine("Done in {0:F4} -- added value {1} in dictionary -- pause....", watch.Elapsed.TotalSeconds, intero);
        Thread.Sleep(4000);
        Console.WriteLine("Second method: TryGetValue");
        watch = Stopwatch.StartNew();
        intero=0;
        int result=0;
        while(testdict.TryGetValue(intero, out result))
        {
            intero++;
        }
        testdict.Add(intero, m_Rand.Next());
        watch.Stop();
        Console.WriteLine("Done in {0:F4} -- added value {1} in dictionary -- pause....", watch.Elapsed.TotalSeconds, intero);
        Thread.Sleep(4000);
        Console.WriteLine("Test hashtable");
        watch = Stopwatch.StartNew();
        intero=0;
        while(testhash.Contains(intero))
        {
            intero++;
        }
        testhash.Add(intero, m_Rand.Next());
        watch.Stop();
        Console.WriteLine("Done in {0:F4} -- added value {1} into hashtable -- pause....", watch.Elapsed.TotalSeconds, intero);
        Console.Write("Press any key to continue . . . ");
        Console.ReadKey(true);
    }
}
}

Este es un verdadero ejemplo, tengo un servicio que para cada "Artículo" creado, asocia un número progresivo, este número, cada vez que crea un nuevo artículo, debe encontrarse libre, si elimina un Artículo, el número libre se convierte en gratis, por supuesto, esto no está optimizado, ya que tengo una var estática que almacena en caché el número actual, pero en caso de que finalice todos los números, puede volver a comenzar de 0 a UInt32.MaxValue

Prueba ejecutada:
Agregando elementos a la tabla hash ...
Hecho en 0,5908 - pausa ....
Agregando elementos al diccionario ...
Hecho en 0,2679 - pausa ....
Encontrando el primer número libre para inserción
Primer método : ContainsKey
Hecho en 0,0561 - valor agregado 1000000 en diccionario - pausa ....
Segundo método: TryGetValue
Hecho en 0,0643 - valor agregado 1000001 en diccionario - pausa ....
Prueba de tabla hash
Hecho en 0, 3015 - valor agregado 1000000 en tabla hash - pausa ....
Presione cualquier tecla para continuar. .

Si algunos de ustedes se preguntan si las ContainsKeys podrían tener una ventaja, incluso he intentado invertir el TryGetValue con la clave Contains, el resultado es el mismo.

Entonces, para mí, con una consideración final, todo depende de la forma en que se comporta el programa.

Fwiffo
fuente