什么更有效:字典TryGetValue或ContainsKey + Item?

时间:2012-02-21 18:01:50

标签: c# performance dictionary

来自MSDN在Dictionary.TryGetValue Method上的条目:

  

此方法结合了ContainsKey方法的功能   Item属性。

     

如果未找到密钥,则value参数将获得相应的参数   值类型TValue的默认值;例如,0(零)表示   整数类型,布尔类型为false,引用类型为null。

     

如果您的代码经常尝试访问,请使用TryGetValue方法   不在字典中的键。使用这种方法更多   比捕获Item抛出的KeyNotFoundException更有效   属性。

     

此方法接近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);

注意:我不是在寻找基准!

10 个答案:

答案 0 :(得分:277)

TryGetValue会更快。

ContainsKey使用与TryGetValue相同的检查,其内部指实际的输入位置。 Item属性实际上具有与TryGetValue几乎完全相同的代码功能,除了它将抛出异常而不是返回false。

使用ContainsKey后跟Item基本上会复制查找功能,这是本案例中的大部分计算。

答案 1 :(得分:85)

快速基准显示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

答案 2 :(得分:45)

由于到目前为止没有一个答案真正回答了这个问题,所以这是我在一些研究后找到的可接受的答案:

如果您反编译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加上数组查找。

Source

似乎TryGetValue的速度几乎是ContainsKey + Item组合的两倍。

答案 3 :(得分:18)

谁在乎: - )

你可能会问,因为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) 

答案 4 :(得分:10)

你为什么不测试它?

但我很确定TryGetValue更快,因为它只进行一次查找。当然,这不能保证,即不同的实现可能具有不同的性能特征。

我实现字典的方法是创建一个内部Find函数,找到项目的插槽,然后在其上构建其余部分。

答案 5 :(得分:10)

到目前为止,所有的答案虽然很好,却错过了重要的一点。

API类(例如.NET框架)的方法构成了接口定义的一部分(不是C#或VB接口,而是计算机科学中的接口)。

因此,询问调用这样的方法是否更快是不正确的,除非速度是正式接口定义的一部分(在这种情况下不是这样)。

传统上,无论语言,基础架构,操作系统,平台或机器架构如何,这种快捷方式(组合搜索和检索)都更有效。它也更具可读性,因为它明确地表达了你的意图,而不是暗示它(来自你的代码结构)。

所以答案(来自一个头发花白的老黑客)肯定是'是'(TryGetValue比ContainsKey和Item [Get]的组合更可取从Dictionary中检索一个值。)

如果您认为这听起来很奇怪,可以这样想:即使TryGetValue,ContainsKey和Item [Get]的当前实现没有产生任何速度差异,您也可以假设它可能是未来的实现(例如。 NET v5)会做(TryGetValue会更快)。考虑一下软件的生命周期。

另外,值得注意的是,典型的现代接口定义技术仍然很少提供正式定义时序约束的任何方法。也许是.NET v5?

答案 6 :(得分:5)

制作一个快速测试程序,使用TryGetValue和字典中的100万个项目肯定会有所改进。

<强>结果:

包含10000次点击的键+项目:45ms

TryGetValue 1000000次点击:26ms

以下是测试应用:

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

}

答案 7 :(得分:4)

在我的机器上,有大量RAM,当在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();
            }
        }
    }

这是我的代码:

{{1}}

答案 8 :(得分: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);
    }
}
}

这是一个真实的例子,我有一个服务,对于每个“Item”创建,它关联一个渐进数字,这个数字,每次你创建一个新项目,必须找到空闲,如果你删除一个项目,免费号码变得免费,当然这没有优化,因为我有一个静态var来缓存当前号码,但如果你结束所有号码,你可以从0重新开始到UInt32.MaxValue

执行测试:
将元素添加到哈希表中...
在0,5908完成 - 暂停....
将元素添加到字典中...
完成0,2679 - 暂停....
找到第一个插入的免费号码
第一种方法:ContainsKey
在0,0561中完成 - 在字典中添加值1000000 - 暂停....
第二种方法:TryGetValue
在0,0643中完成 - 在字典中添加值1000001 - 暂停....
测试哈希表
在0,3015完成 - 在哈希表中增加值1000000 - 暂停....
按任意键继续 。 。

如果你们中的一些人可能会问ContainsKeys是否有优势,我甚至尝试用Contains键反转TryGetValue,结果是一样的。

所以,对我来说,最后一点考虑,一切都取决于程序的行为方式。

答案 9 :(得分:2)

除了设计可在实际设置中提供准确结果的微基准外,您还可以检查.NET Framework的参考源。

他们都调用执行大部分工作的FindEntry(TKey)方法而不会记住其结果,因此调用TryGetValue的速度几乎是ContainsKey + {的两倍{1}}

Item 的不方便界面可以使用扩展方法进行调整

TryGetValue

自C#7.1起,您可以将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; } } } 替换为普通default(TValue)The type is inferred.

用法:

default

对于查找失败的引用类型,它返回var dict = new Dictionary<string, string>(); string val = dict.GetValueOrDefault("theKey", "value used if theKey is not found in dict"); ,除非指定了显式默认值。

null