【问题标题】:What is more efficient: Dictionary TryGetValue or ContainsKey+Item?哪个更有效:字典 TryGetValue 或 ContainsKey+Item?
【发布时间】:2012-03-12 01:34:41
【问题描述】:

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

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

如果没有找到key,那么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);

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

【问题讨论】:

    标签: c# performance dictionary


    【解决方案1】:

    TryGetValue 会更快。

    ContainsKey 使用与TryGetValue 相同的检查,在内部引用实际条目位置。 Item 属性实际上与 TryGetValue 具有几乎相同的代码功能,只是它会抛出异常而不是返回 false。

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

    【讨论】:

    • 这更微妙:if(dict.ContainsKey(ikey)) dict[ikey]++; else dict.Add(ikey, 0);。但我认为TryGetValue 仍然更有效,因为使用了索引器属性的 get and 集,不是吗?
    • 您现在实际上也可以查看它的 .net 源代码:referencesource.microsoft.com/#mscorlib/system/collections/… 您可以看到所有 3 个 TryGetValue、ContainsKey 和 this[] 调用相同的 FindEntry 方法并执行相同的数量工作,只是他们回答问题的方式不同:trygetvalue返回bool和value,包含key只返回true/false,this[]返回值或抛出异常。
    • @JohnGardner 是的,这就是我所说的 - 但如果你执行 ContainsKey 然后获取项目,那么你所做的工作是 2 倍而不是 1 倍。
    • 我完全同意 :) 我只是指出实际来源现在可用。其他答案/等都没有指向实际来源的链接:D
    • 稍微偏离主题,如果您在多线程环境中通过 IDictionary 访问,我将始终使用 TryGetValue,因为状态可能会从您调用 ContainsKey 时发生变化(无法保证 TryGetValue 将在内部正确锁定要么,但它可能更安全)
    【解决方案2】:

    快速基准测试表明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
    

    【讨论】:

    • @Luciano 解释你如何使用Any - 像这样:Any(i=&gt;i.Key==key)。在这种情况下,是的,这是一个糟糕的字典线性搜索。
    • DateTime.Now 只会精确到几毫秒。改用System.Diagnostics 中的Stopwatch 类(它在后台使用QueryPerformanceCounter 以提供更高的准确性)。它也更容易使用。
    • 除了 Alastair 和 Ed 的 cmets - DateTime.Now 可以倒退,如果你得到一个时间更新,例如当用户更新他们的计算机时间时发生的,一个时区被跨越,或者时区更改(例如 DST)。尝试在系统时钟与某些无线电服务(如 GPS 或移动电话网络)提供的时间同步的系统上工作。 DateTime.Now 将无处不在,而 DateTime.UtcNow 仅修复其中一个原因。只需使用秒表。
    • @Dan 我要比较的两个操作都必须是 O(1),这不是基准测试的重点。
    • @Dan 我的基准测试还会对操作进行一千万次迭代以获得实际结果。此外,我的结果与其他人得到的结果非常一致:例如,davisoa 的 45/26 比率在我的 0.811/0.493 比率的 5% 以内。
    【解决方案3】:

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

    如果你反编译 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 组合的两倍。

    【讨论】:

      【解决方案4】:

      谁在乎 :-)

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

      【讨论】:

      • @Hüseyin 我很困惑我是如何愚蠢到在没有this 的情况下发布这个但事实证明我的方法在我的代码库中重复了两次——一次有,一次没有this所以这就是为什么我从来没有抓住它!感谢修复!
      • TryGetValue 如果键不存在,则将默认值分配给 out value 参数,因此可以简化。
      • 简化版:public static TValue GetValueOrDefault(this Dictionary dict, TKey key) { TValue ret; dict.TryGetValue(key, out ret);返回 ret; }
      • 在 C#7 中这真的很有趣:if(!dic.TryGetValue(key, out value item)) item = dic[key] = new Item();
      • 讽刺的是,真正的源代码已经有一个 GetValueOrDefault() 例程,但它被隐藏了...referencesource.microsoft.com/#mscorlib/system/collections/…
      【解决方案5】:

      你为什么不测试一下?

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

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

      【讨论】:

      • 我认为实现细节不可能改变执行X动作一次快于或等于执行X动作两次的保证。最好的情况是相同的,最坏的情况是 2X 版本需要两倍的时间。
      【解决方案6】:

      到目前为止的所有答案,虽然很好,但都错过了一个关键点。

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

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

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

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

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

      顺便说一句,有趣的是,典型的现代接口定义技术仍然很少提供任何正式定义时序约束的方法。也许是 .NET v5?

      【讨论】:

      • 虽然我 100% 同意您关于语义的论点,但仍然值得进行性能测试。除非您进行测试,否则您永远不知道您使用的 API 何时具有次优的实现,以至于语义正确的事情碰巧变慢了。
      【解决方案7】:

      除了设计一个可以在实际设置中给出准确结果的微基准测试之外,您还可以检查 .NET Framework 的参考源。

      他们都调用了FindEntry(TKey) 方法,该方法完成了大部分工作并且不记忆其结果,因此调用TryGetValue 的速度几乎是ContainsKey + Item 的两倍 .


      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) 替换为普通的 defaultThe type is inferred.

      用法:

      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);
      
      var dictInt = new Dictionary<string, int>();
      int valInt = dictInt.GetValueOrDefault("nonexistent");
      Debug.Assert(valInt == 0);
      

      【讨论】:

      • 请注意,扩展方法的用户无法区分不存在的键和存在但其值为默认值(T)的键。
      • 在现代计算机上,如果你快速连续调用一个子程序两次,它所花费的时间不可能是调用一次的两倍。这是因为 CPU 和缓存架构很可能会缓存大量与第一次调用相关的指令和数据,因此第二次调用会执行得更快。另一方面,调用两次几乎肯定比调用一次花费的时间要长一些,因此如果可能,消除第二次调用仍然有优势。
      【解决方案8】:

      在我的机器上,有大量 RAM,当在 RELEASE 模式下运行(不是 DEBUG)时,ContainsKey 等于 TryGetValue/try-catch,如果找到了 Dictionary&lt;&gt; 中的所有条目。

      当只找到几个字典条目时,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() 的实际作用一无所知。可能会产生副作用,所以必须调用,不能删减。
      • 这里有些东西是假的。事实上,检查 .NET 代码显示 ContainsKey、TryGetValue 和 this[] 都调用相同的内部代码,因此当条目存在时,TryGetValue 比 ContainsKey + this[] 快。
      【解决方案9】:

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

      结果:

      包含 1000000 次点击的密钥 + 项目:45 毫秒

      1000000 次点击的 TryGetValue: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);
      
      }
      

      【讨论】:

        【解决方案10】:

        如果您尝试从字典中取出值,TryGetValue(key, out value) 是最好的选择,但如果您要检查键是否存在,则进行新插入,而不会覆盖旧键,并且仅在该范围内, ContainsKey(key) 是最佳选择,基准可以确认这一点:

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

        这是一个真实的例子,我有一个服务,它为每个创建的“项目”关联一个累进数字,这个数字,每次创建新项目时,必须免费找到,如果你删除一个项目, free number 变free了,当然​​这个没有优化,因为我有一个静态var缓存当前的number,但是万一你把所有的number都结束了,可以从0重新开始到UInt32.MaxValue

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

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

        所以,对我来说,最后考虑一下,这完全取决于程序的行为方式。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2017-02-14
          • 2012-04-19
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多