Що ефективніше: словник TryGetValue або ContainsKey + Item?


251

З моменту запису MSDN на Dictionary.TryGetValue Метод :

Цей метод поєднує функціональність методу ContainsKey та властивості Item.

Якщо ключ не знайдено, то параметр значення отримує відповідне значення за замовчуванням для типу значення TValue; наприклад, 0 (нуль) для цілих типів, false для булевих типів і null для типів посилань.

Використовуйте метод TryGetValue, якщо ваш код часто намагається отримати доступ до ключів, яких немає у словнику. Використання цього методу є більш ефективним, ніж ловити KeyNotFoundException, кинутого властивістю Item.

Цей метод наближається до операції O (1).

З опису незрозуміло, чи це більш ефективно чи просто зручніше, ніж телефонувати на ContainsKey, а потім робити пошук. Чи реалізація TryGetValueпросто виклику ContainsKey, а потім Item або є фактично більш ефективною, ніж це, здійснюючи один пошук?

Іншими словами, що є більш ефективним (тобто, який здійснює менше пошуку):

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

або

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

Примітка: Я не шукаю орієнтиру!

Відповіді:


313

TryGetValue буде швидше.

ContainsKeyвикористовує той самий чек TryGetValue, що і внутрішньо посилається на фактичне місце входу. ItemВластивість на насправді має майже ідентичну функціональність коду , як TryGetValue, за винятком того, що він буде кидати виняток замість повернення БРЕХНІ.

Використання з ContainsKeyподальшим поданням в Itemосновному дублює функціональність пошуку, що становить основну частину обчислень у цьому випадку.


2
Це більш тонке: if(dict.ContainsKey(ikey)) dict[ikey]++; else dict.Add(ikey, 0);. Але я думаю, що TryGetValueце все-таки ефективніше, оскільки використовується get і set властивості індексатора, чи не так?
Тім Шмелтер

4
Ви також можете переглянути джерело .net для нього і зараз: referenceource.microsoft.com/#mscorlib/system/collections/… Ви можете бачити, що всі 3 TryGetValue, ContainsKey, і це [] називають той самий метод FindEntry і робити той самий обсяг роботи, лише відрізняється тим, як вони відповідають на питання: trygetvalue повертає bool та значення, містить ключ, який повертає лише true / false, і це [] повертає значення або кидає виняток.
Джон Гарднер

1
@JohnGardner Так, це те, що я сказав - але якщо ви маєте ContainsKey, то отримайте Item, ви робите цю роботу 2 рази замість 1x.
Рід Копсей

3
Я згоден повністю :) Я просто вказував, що фактичне джерело доступне вже зараз. жодна з інших відповідей / тощо не мала посилання на власне джерело: D
Джон Гарднер

1
Злегка поза темою, якщо ви звертаєтесь через IDictionary в багатопотокове середовище, я завжди використовую TryGetValue, оскільки стан може змінитися з часу, коли ви телефонуєте ContainsKey (немає гарантії, що TryGetValue буде внутрішньо заблокований правильно, але це, мабуть, безпечніше)
Кріс Беррі

91

Швидкий показник показує, що TryGetValueмає невеликий край:

    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);
            }
        }
   }

Це виробляє

00:00:00.7600000
00:00:01.0610000

роблячи ContainsKey + Itemдоступ приблизно на 40% повільнішим, передбачаючи рівномірну суміш ударів і промахів.

Більше того, коли я змінюю програму, щоб завжди пропускати (тобто завжди шукати "b"), дві версії стають однаково швидкими:

00:00:00.2850000
00:00:00.2720000

Однак, коли я встигаю "всі хіти", я залишаюсь TryGetValueочевидним переможцем:

00:00:00.4930000
00:00:00.8110000

11
Звичайно, це залежить від фактичної схеми використання. Якщо ви майже ніколи не провалюєте пошук, то ви TryGetValueповинні бути далеко вперед. Крім того ... нітрик ... DateTime- не найкращий спосіб фіксувати показники продуктивності.
Ред С.

4
@EdS. Ви маєте рацію, TryGetValueотримуєте ще більше лідерства. Я відредагував відповідь, щоб включити сценарії "всі звернення" та "всі пропуски".
dasblinkenlight

2
@Luciano пояснити , як ти Any- Як це: Any(i=>i.Key==key). У такому випадку, так, це поганий лінійний пошук словника.
weston

13
DateTime.Nowбуде точним лише до декількох мс. Замість цього використовуйте Stopwatchклас System.Diagnostics(який використовує QueryPerformanceCounter під обкладинками, щоб забезпечити набагато більшу точність). Це і простіше у використанні.
Аластер Мау

5
На додаток до коментарів Alastair та Ed - DateTime.Now може піти назад, якщо ви отримаєте оновлення часу, наприклад, яке відбувається, коли користувач оновлює час свого комп'ютера, перетинається часовий пояс або змінюється часовий пояс (DST, for екземпляр). Спробуйте працювати над системою, синхронізація якої синхронізує час із часом, яку надає деяка радіослужба, наприклад, GPS або мережі мобільного телефону. DateTime.Now пройде повсюдно, а DateTime.UtcNow виправляє лише одну з цих причин. Просто використовуйте StopWatch.
антидух

51

Оскільки жодна з відповідей поки що насправді не відповідає на це питання, ось прийнятну відповідь, яку я знайшов після деяких досліджень:

Якщо декомпілювати TryGetValue, ви побачите, що це робиться так:

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;
}

тоді як метод ContainsKey:

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

тому TryGetValue - це просто ContainsKey плюс пошук масиву, якщо елемент присутній.

Джерело

Схоже, TryGetValue буде майже вдвічі швидшим, ніж комбінація ContainsKey + Item.


20

Кого хвилює :-)

Ви, мабуть, запитуєте, тому що TryGetValueце біль - це інкапсулювати його таким способом розширення.

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;
    }

Тоді просто зателефонуйте:

dict.GetValueOrDefault("keyname")

або

(dict.GetValueOrDefault("keyname") ?? fallbackValue) 

1
@ Hüseyin Я дуже розгубився, як я був досить дурним, щоб розмістити це без цього, thisале, виявляється, у мене метод два рази повторюється в моїй кодовій базі - один раз і один без thisцього, тому я ніколи його не спіймав! дякую за виправлення!
Simon_Weaver

2
TryGetValueприсвоює параметр вихідного значення за замовчуванням, якщо ключ не існує, тому це може бути спрощено.
Рафаель Сміт

2
Спрощена версія: загальнодоступна статична TValue GetValueOrDefault <TKey, TValue> (цей словник <TKey, TValue> dict, ключ TKey) {TValue ret; dict.TryGetValue (ключ, вихід ret); повернути рет; }
Джошуа

2
У C # 7 це дійсно весело:if(!dic.TryGetValue(key, out value item)) item = dic[key] = new Item();
Shimmy Weitzhandler,

1
За іронією долі, справжній вихідний код уже має процедуру GetValueOrDefault (), але це приховано ... referenceource.microsoft.com/#mscorlib/system/collections/…
Deven T.

10

Чому б не випробувати його?

Але я майже впевнений у цьому TryGetValue це швидше, тому що це лише один пошук. Звичайно, це не гарантується, тобто різні реалізації можуть мати різні характеристики продуктивності.

Те, як я реалізую словник, полягає в створенні внутрішньої Findфункції, яка знаходить слот для елемента, а потім будує решту поверх цього.


Я не думаю, що деталі реалізації можуть, можливо, змінити гарантію того, що виконання дії X один раз швидше або рівне виконання дії X вдвічі. У кращому випадку вони однакові, гірше, якщо версія 2X займає вдвічі довше.
Ден Бешард

9

Усі відповіді на даний момент, хоча і хороші, але пропускають життєво важливий момент.

Методи до класів API (наприклад, .NET Framework) складають частину визначення інтерфейсу (не інтерфейс C # або VB, а інтерфейс у значенні інформатики).

Як правило, зазвичай неправильно запитувати, чи викликає такий метод швидше, якщо тільки швидкість не є частиною формального визначення інтерфейсу (чого це не в цьому випадку).

Традиційно цей тип ярликів (поєднує пошук і завантаження) є більш ефективним незалежно від мови, інфраструктури, ОС, платформи чи машинної архітектури. Він також більш читабельний, тому що він виражає ваш намір явно, а не нав'язує його (із структури вашого коду).

Отже, відповідь (зі скрегоченого старого хака), безумовно, "так" (TryGetValue є кращим порівняно з комбінацією ContainsKey та Item [Get], щоб отримати значення зі словника).

Якщо ви думаєте, що це здається дивним, подумайте про це так: Навіть якщо поточні реалізації TryGetValue, ContainsKey та Item [Get] не дають різниці в швидкості, ви можете припустити, що це може бути майбутня реалізація (наприклад, .NET v5) зробить (TryGetValue буде швидше). Подумайте про термін експлуатації вашого програмного забезпечення.

Як осторонь, цікаво відзначити, що типові сучасні технології визначення інтерфейсу все ще рідко надають будь-які засоби формального визначення обмежень у часі. Можливо .NET v5?


2
Хоча я на 100% погоджуюся з вашим аргументом щодо семантики, все-таки варто зробити тест на ефективність. Ви ніколи не знаєте, коли API, який ви використовуєте, має неоптимальну реалізацію таким чином, що семантично правильна річ відбувається повільніше, якщо ви не зробите тест.
Ден Бешард

5

Зробити швидку програму тестування, безумовно, є покращення, використовуючи TryGetValue з 1 мільйоном елементів у словнику.

Результати:

Містить ключ + предмет для 1000000 звернень: 45 мс

TryGetValue для 1000000 звернень: 26 мс

Ось тестовий додаток:

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);

}

5

На моїй машині з навантаженнями оперативної пам’яті при запуску в режимі RELEASE (а не DEBUG) ContainsKeyдорівнює TryGetValue/ try-catchякщо всі записи у Dictionary<>знайдені.

ContainsKeyперевершує їх усе далеко, коли не знайдено лише декількох записів у словнику (у моєму прикладі нижче встановлено MAXVALщось більше, ніж ENTRIESпропущено деякі записи):

Результати:

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

Ось мій код:

    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();
            }
        }
    }

Я відчуваю, що щось рибне відбувається тут. Цікаво, чи може оптимізатор видалити чи спростити ваші чеки ContainsKey () через те, що ви ніколи не використовуєте отримане значення.
Ден Бешард

Це просто не може. ContainsKey () знаходиться у складеній DLL. Оптимізатор нічого не знає про те, що насправді робить ContainsKey (). Це може спричинити побічні ефекти, тому його потрібно викликати і не можна скорочувати.
AxD

Щось тут хитре. Справа в тому, що вивчення коду .NET показує, що ContainsKey, TryGetValue і це [] всі викликають один і той же внутрішній код, тому TryGetValue швидше, ніж ContainsKey + це [], коли запис існує.
Джим Балтер

3

Окрім розробки мікробензика, який дасть точні результати у практичній обстановці, ви можете ознайомитись із еталонним джерелом .NET Framework.

Усі вони називають FindEntry(TKey) метод, який виконує більшу частину роботи і не запам'ятовує його результат, тому дзвінки TryGetValueмайже вдвічіContainsKeyItem швидші, ніж + .


Незручний інтерфейс TryGetValue можна адаптувати за допомогою методу розширення :

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;
        }
    }
}

Оскільки C # 7.1, ви можете замінити default(TValue)на звичайний default. Тип висновку.

Використання:

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

Він повертається nullдля типів посилань, пошук яких не вдається, якщо не вказано явне значення за замовчуванням.

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);

Зауважте, що користувачі методу розширення не можуть визначити різницю між неіснуючим ключем і наявним ключем, але його значенням є за замовчуванням (T).
Лукас

На сучасному комп’ютері, якщо ви зателефонуєте до підпрограми двічі за швидкою послідовністю, навряд чи це займе вдвічі більше часу, ніж викликати її один раз. Це тому, що процесор і архітектура кешування дуже ймовірно кешують багато інструкцій та даних, пов’язаних з першим викликом, тому другий виклик буде виконуватися швидше. З іншого боку, виклик двічі майже певний час може зайняти трохи більше часу, ніж один раз, тому все-таки є перевага усунути другий виклик, якщо це можливо.
дебат

2

Якщо ви намагаєтесь отримати значення зі словника, TryGetValue (ключ, значення) - найкращий варіант, але якщо ви перевіряєте наявність ключа, на нову вставку, не перезаписуючи старі ключі, і тільки за допомогою цієї області, ContainsKey (ключ) - найкращий варіант, тест може підтвердити це:

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);
    }
}
}

Це справжній приклад. У мене є служба, що для кожного створеного "Елемента" він пов'язує прогресивне число, це число кожного разу, коли ви створюєте новий елемент, повинно знаходитись безкоштовно, якщо ви видаляєте елемент, вільний номер стає безкоштовно, звичайно, це не оптимізовано, оскільки у мене є статичний var, який кешує поточне число, але у випадку, якщо ви закінчите всі числа, ви можете знову почати з 0 до UInt32.MaxValue

Тест виконаний:
Додавання елементів у хештеб ...
Зроблено в 0,5908 - пауза ....
Додавання елементів у словник ...
Зроблено в 0,2679 - пауза ....
Пошук першого вільного числа для вставки
Перший метод : Містить Ключ, зроблений
в 0,0561 - додана вартість 1000000 в словнику - пауза ....
Другий метод: TryGetValue
Виконано в 0,0643 - додана вартість 1000001 в словнику - пауза ....
Тест хештелю
Готово 0, 3015 - додана вартість 1000000 у хештелі - пауза ....
Натисніть будь-яку клавішу для продовження. .

Якщо хтось із вас може запитати, чи могли б ContainsKeys мати перевагу, я навіть намагався перевернути TryGetValue з ключем Contains, результат такий же.

Отже, для мене, з остаточним врахуванням, все залежить від того, як поводиться програма.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.