【问题标题】:.NET Framework - Any way to get Dictionary<> to be a little faster?.NET Framework - 有什么方法可以让 Dictionary<> 更快一点?
【发布时间】:2010-08-09 05:31:07
【问题描述】:

我正在 O(n^2) 循环中进行Dictionary&lt;&gt; 查找,并且需要它快得离谱。它不是。有人对Dictionary&lt;&gt; 的实现方式有任何见解吗?在通过分析器运行我的代码并确定字典查找是 CPU 时间的大部分之后,我正在使用一个隔离的测试用例测试字典性能。我的测试代码是这样的:

Int32[] keys = new Int32[10] { 38784, 19294, 109574, 2450985, 5, 398, 98405, 12093, 909802, 38294394 };

Dictionary<Int32, MyData> map = new Dictionary<Int32, MyData>();
//Add a bunch of things to map


timer.Start();
Object item;
for (int i = 0; i < 1000000; i++)
{
   for (int j = 0; j < keys.Length; j++)
   {
      bool isFound = map.ContainsKey(keys[j]);
      if (isFound)
      {
         item = map[keys[j]];
      }
   }
}
timer.Stop();

ContainsKey 和 map[] 是两个慢的部分(同样慢)。如果我添加一个 TryGetValue,它的速度几乎与 ContainsKey 相同。这里有一些有趣的事实..

Dictionary&lt;Guid, T&gt; 的速度大约是 Dictionary&lt;Int32, T&gt; 的两倍。 Dictionary&lt;String, T&gt; 的速度大约是 Guid 字典的两倍。 Dictionary&lt;Byte, T&gt; 比使用 Ints 快 50%。这让我相信 Dictionary 正在执行 O(log n) 二进制搜索来查找键,而键上的比较运算符是瓶颈。出于某种原因,我不认为它是作为 Hashtable 实现的,因为 .NET 已经有一个 Hashtable 类,而且根据我的经验,它甚至比 Dictionary 还要慢。

我正在构建的字典一次只能由一个线程访问,因此读取锁定不是问题。 RAM 也不是问题。字典很可能只有大约 10 个桶,但每个桶可以指向大约 2,000 个可能的事物之一。有人对如何加快速度有任何反馈吗?谢谢!

迈克

【问题讨论】:

  • "如果我添加 TryGetValue,它的速度几乎与 ContainsKey 相同。" TryGetValue 与 ContainsKey 的速度相同,但同时返回项目,节省了第二次查找以获取值。您没有看到这种改进吗?
  • 您可以使用 .NET Reflector 查看实际实现。但老实说,也许你的算法是问题所在。你需要做那么多查找吗?

标签: c# .net algorithm performance hashtable


【解决方案1】:

字典是使用哈希表实现的,我已经看过使用 Reflector 的代码。

"字典很可能只 大约有 10 个桶,但每个桶 可以指向大约 2,000 个中的一个 可能的东西。”

你的问题。字典使用哈希来定位桶,但是桶中的查找是线性的。

您必须实现具有更好分布的哈希算法才能获得更好的性能。这种关系至少应该是相反的,即 2000 个桶,每个桶有 10 个项目。

【讨论】:

  • +1 - 正是问题所在。哈希算法完全是无聊的(如:效率低下且未正确实施)。
  • 嗯,这就解释了为什么 Int32 快得多.. Int32 只是将自己用于哈希码,其中 Guid 和 String 可能包含一些冲突.. 我对 Int32 字典的测试可以做大约 10M在 900 毫秒内查找,这可能和我能得到的一样好..
  • 所以让我直截了当。当多个键“散列”到同一个存储桶时,字典会变慢,对吗?一旦你解决了这个问题,查找是 O(1)?这基本上回答了我的问题..
  • 好吧,这似乎工作了,一旦我在我的键上实现 GetHashCode,我的速度与使用 Int32 键的字典大致相同。
  • Mike:在上面的示例中,使用 .Net 4.0,map 是一个包含 17 个存储桶的哈希表,其中 7 个已填充。这意味着每查找 10 个键,就会有 3 个发生冲突。在您的情况下,存储桶 9 中的冲突为 98405、109574,存储桶 7 中的冲突为 38294394、398、38784。这意味着 38784 需要 3 次比较,109574 和 398 需要 2 次比较,所有其他查找都需要 1 次比较。
【解决方案2】:

添加到 cmets 关于基于了解数据创建自己的实现,这是一个不会发生冲突的示例。这可能会根据对象的大小抛出 OutOfMemoryExceptions。我尝试使用 int 索引器,但这会引发 OutOfMemoryException。如果返回 null,则该项目不存在。

我没有对此进行分析,但我预计速度会有所改善,但内存使用量会更大。

public class QuickLookup<T> where T : class
{
    private T[] _postives = new T[short.MaxValue + 1];
    private T[] _negatives = new T[short.MaxValue + 1];
    public T this[short key]
    {
        get
        {
            return key < 0 ? _negatives[(key * -1) - 1] : _postives[key];
        }
        set
        {
            if (key < 0)
                _negatives[key * -1] = value;
            else
                _postives[key] = value;
        }
    }
}

【讨论】:

    【解决方案3】:

    如果您只有 10 个存储桶,每个存储桶包含 2000 件东西,您是否可以只构建一个包含所有 20000 件东西的列表,可以直接通过循环已知的键对其进行索引?例如:

    List<MyData> = new List(); 
    
    //add all items to list indexed by their key (RAM is not an issue right?)
    
    item = ItemList[key];
    

    通过这种方式,您可以直接引用它们,而无需查找字典或哈希。

    【讨论】:

    • 是的,但是构建该列表将花费太长时间。我这样做了几十万次..
    • List是如何实现的?如果不做哈希,查找 O(N) 吗?
    • 哦,愚蠢的问题,List 像数组一样按偏移量索引.. 但是当您传入比较函数时,我猜 Contains 是 O(N) 或 O(log N)。跨度>
    • Mike:包含 O(N); BinarySearch 是 O(lgN)
    • 是的,我想这就是为什么 List.Contains() 有一个覆盖来传递比较器,然后他们可以进行二进制搜索.. 这是有道理的..
    【解决方案4】:

    听起来您是在说您的字典中只有 10 个项目。如果是这样,哈希表可能是没有根据的。您可以将数据存储在列表/数组中,然后对其进行迭代或使用二进制搜索来查找您的密钥(尝试两者来查看更快的方法)。

    如果您使用二分查找,您的列表必须进行排序;如果您只是遍历您的列表并且有一些键比其他键更频繁地查找,您可以将它们放在列表的开头以加快速度。

    另一方面,如果您的密钥是预先知道的,您可以编写自己的哈希表实现,具有快速和完美的哈希函数(即没有冲突),这应该是无与伦比的。

    【讨论】:

    • 你怎么看?哈希表不应该是 O(1) 吗?这听起来比迭代大约 10 项要好。. 进行二进制搜索以查找键意味着对我的数据结构进行排序,这是可能的,但需要做一些工作.. 从理论上讲,哈希表应该仍然更快..如果构建字典很慢,我会在分析时看到..
    • 你只有 10 个键,对吧?这意味着您的查找将是 O(10),这与 O(1) 相同。换句话说,您的计算机迭代 10 个项目的列表可能比它调用散列函数和执行索引到散列表所需的整数除法更快。
    • 哦,是的,我现在明白了.. 好点,在散列真正得到回报之前,字典中可能有最少数量的键.. 我一定会运行一些测试来证实这个理论..
    • 刚刚尝试过,即使字典中有 10 个值,计算哈希值也比 O(10) 查找快得多 MUUUUUCH。1786 毫秒对 437 毫秒。
    • 哦,另外,我的速度大约是发布版本的两倍。JIT'er 一定是在做一些漂亮的内联或什么的......
    【解决方案5】:

    对哈希表的内部工作原理的洞察力是正确的。您绝对应该使用 TryGetValue 作为整个内部循环:

      map.TryGetValue(keys[j], out item);
    

    做 ContainsKey 和 Item[] 是在做困难的部分(查找)两次。一个额外的 if 和一个额外的 keys[j] 是次要的,但会在一个紧密的循环中加起来。在你的键上使用 foreach 可能会更慢,但根据循环的实际内容,它可能值得分析。

    【讨论】:

      猜你喜欢
      • 2020-06-02
      • 1970-01-01
      • 2010-12-21
      • 1970-01-01
      • 1970-01-01
      • 2013-04-21
      • 2015-04-25
      • 2011-03-01
      • 1970-01-01
      相关资源
      最近更新 更多