【问题标题】:Optimizing Lookups: Dictionary key lookups vs. Array index lookups优化查找:字典键查找与数组索引查找
【发布时间】:2010-10-28 19:51:03
【问题描述】:

我正在编写一个 7 张牌扑克手评估器,作为我最喜欢的项目之一。在尝试优化其速度时(我喜欢这个挑战),我震惊地发现字典键查找的性能与数组索引查找相比非常慢。

例如,我运行了这个示例代码,它枚举了所有 52 个选择 7 = 133,784,560 个可能的 7 张牌:

var intDict = new Dictionary<int, int>();
var intList = new List<int>();
for (int i = 0; i < 100000; i ++)
{
    intDict.Add(i, i);  
    intList.Add(i);
}

int result;

var sw = new Stopwatch();
sw.Start();
for (int card1 = 0; card1 < 46; card1++)
  for (int card2 = card1 + 1; card2 < 47; card2++)
    for (int card3 = card2 + 1; card3 < 48; card3++)
      for (int card4 = card3 + 1; card4 < 49; card4++)
        for (int card5 = card4 + 1; card5 < 50; card5++)
          for (int card6 = card5 + 1; card6 < 51; card6++)
            for (int card7 = card6 + 1; card7 < 52; card7++)
              result = intDict[32131]; // perform C(52,7) dictionary key lookups
sw.Stop();
Console.WriteLine("time for dictionary lookups: {0} ms", sw.ElapsedMilliseconds);

sw.Reset();

sw.Start();
for (int card1 = 0; card1 < 46; card1++)
  for (int card2 = card1 + 1; card2 < 47; card2++)
    for (int card3 = card2 + 1; card3 < 48; card3++)
      for (int card4 = card3 + 1; card4 < 49; card4++)
        for (int card5 = card4 + 1; card5 < 50; card5++)
          for (int card6 = card5 + 1; card6 < 51; card6++)
            for (int card7 = card6 + 1; card7 < 52; card7++)
              result = intList[32131]; // perform C(52,7) array index lookups
sw.Stop();
Console.WriteLine("time for array index lookups: {0} ms", sw.ElapsedMilliseconds);

哪个输出:

time for dictionary lookups: 2532 ms
time for array index lookups: 313 ms

是否会出现这种行为(性能下降 8 倍)? IIRC,字典平均有 O(1) 查找,而数组有最坏情况 O(1) 查找,所以我确实希望数组查找更快,但不会这么快!

我目前将扑克手牌排名存储在字典中。我想如果这与字典查找一样快,我必须重新考虑我的方法并改用数组,尽管索引排名会有点棘手,我可能不得不问另一个问题。

【问题讨论】:

  • f 字典查找总是需要 1 小时才能完成单个项目,并且 总是 一个小时,即使是另一种类型的查找,它仍然是 O(1),例如数组查找,需要 1ms,也是 O(1)。您只能使用 big-O 表示法来比较复杂性,不要用它来实际测量代码的运行时性能特征。
  • 我不认为复杂性不是正确的词,大 O 表示法告诉你它是如何随着项目数量而变化的。 O(1) 告诉您锁定是恒定的,即随着集合的增长,查找不会花费更长的时间。但是,查找可能具有非常高的复杂性,并且仍然像字典一样是 O(1),或者它们可能具有非常低的复杂性,并且像数组查找一样是 O(1)。
  • 我很想知道您的方法与我的 OneJoker 库相比如何。它针对 5 张牌进行了优化,但可以做 7 张,我的查找表只有 1MB 左右,而你的可能接近 GB。

标签: c# .net performance poker


【解决方案1】:

不要忘记,Big-O 表示法仅说明了复杂性如何随大小(等)增长 - 它并没有说明所涉及的常数因素。这就是为什么当键足够少时,有时即使是线性搜索键也比字典查找更快。在这种情况下,您甚至没有对数组进行搜索 - 只是一个直接的索引操作。

对于直接索引查找,数组基本上是理想的 - 这只是

pointer_into_array = base_pointer + offset * size

(然后是指针解引用。)

执行字典查找相对复杂 - 与(例如)在有很多键时按键进行线性查找相比非常快,但比直接数组查找要复杂得多。它必须计算密钥的哈希,然后计算出应该在哪个桶中,可能处理重复的哈希(或重复的桶),然后检查是否相等。

与往常一样,为工作选择正确的数据结构 - 如果您真的可以只对数组(或 List&lt;T&gt;)进行索引,那么是的,这将是非常快的。

【讨论】:

  • 我想问一个关于这个的问题,但不觉得有必要提出自己的问题。如果字符串是不可变的,哈希码是计算一次并存储在内部,还是每次调用 GetHashCode() 时重新计算一次?
  • Chris:在 .NET 中,每次都会重新计算。在 Java 中,它是缓存的。这是基于每个实例的,但是哈希表当然确实记住每个键的哈希值。
  • 字典查找比 Linq 到数据集查找快吗?
  • 是否有任何键限制(在 python 中)以更喜欢线性搜索而不是字典中的键值搜索?
  • @NoahJ.Standerson:我对 Python 了解的不够多,无法给出估计,但这很可能取决于散列密钥的成本以及其他任何事情。与以往一样,在性能方面,请使用真实数据进行衡量。
【解决方案2】:

是否会出现这种行为(性能下降 8 倍)?

为什么不呢?每个数组查找几乎是瞬时的/可以忽略不计,而字典查找可能至少需要一个额外的子例程调用。

两者都是 O(1) 意味着即使每个集合中有 50 倍的项目,性能下降仍然只是 (8) 的一个因素。

【讨论】:

  • 不完全。根本不应该有性能下降!这就是 O(1) 的真正意义。字典确实有一个小脚注说:如果散列函数产生太多重复,或者如果桶太少,那么性能就会下降。
  • @CraigYoung 我的意思是,一个实现(数组)和另一个(字典)之间的性能差异应该保持不变(8 倍),与大小无关。
  • 我明白你的意思。我相信你误解了我的意思。声称 O(1) 算法的任何性能下降都是错误!此外,您归因于 O(1) 的两个不同算法的属性对于 任何 相同 Order 类的两个算法都是正确的。 (即 2 O(n^2) 算法也有一个与大小无关的常数因子;对于 2 O(n!) 等也是如此)。所以你完全错过了 O(1) 的真正点。
  • @CraigYoung 我并不是说性能会随着大小而降低。如果您有两个实现(数组和字典),并且它们都是 O(1),并且当它们都包含时字典比数组慢 8 倍,例如10 个项目,然后我预测当它们都包含 100 或 1000 个项目时,字典仍然比数组慢 8 倍。
  • @CraigYoung “减少”一词在 OP 中并在我的回答中引用。有问题的“减少”是数组和字典之间的速度差异:而不是存储更多项目时的速度差异。
【解决方案3】:

有些东西可能需要一千年,但仍然是 O(1)。

如果您在反汇编窗口中单步执行此代码,您将很快了解其中的区别。

【讨论】:

  • 是的。编写 O(1) 算法的一种简单方法是预先计算最坏时间(即 n 的最大值),然后在同一时间运行它,即使对于集合中最简单的元素(例如,第一个元素)也是如此。收集仍然是 O(1)。您也可能在 O(1) 中被公司解雇 :)
【解决方案4】:

当键空间非常大且无法映射为稳定的有序顺序时,字典结构最有用。如果您可以将键转换为相对较小范围内的简单整数,那么您将很难找到性能比数组更好的数据结构。

关于实施说明;在 .NET 中,字典本质上是可散列的。您可以通过确保您的键散列到大量唯一值空间中来在一定程度上提高它们的键查找性能。在您的情况下,您使用一个简单的整数作为键(我相信它会散列到它自己的值) - 所以这可能是您能做的最好的事情。

【讨论】:

    【解决方案5】:

    数组查找是您可以做的最快的事情 - 基本上它只是从数组的开头到您想要查找的元素的一点指针算术。另一方面,字典查找可能会慢一些,因为它需要进行散列处理并关注找到正确的存储桶。虽然预期的运行时间也是 O(1) - 算法常数更大,所以它会更慢。

    【讨论】:

      【解决方案6】:

      欢迎使用 Big-O 表示法。您始终必须考虑其中涉及一个恒定因素。

      进行一次字典查找当然比数组查找要昂贵得多。

      Big-O 仅告诉您算法如何扩展。将查找次数加倍并查看数字如何变化:两者都应该花费大约两倍的时间。

      【讨论】:

      • 不完全。将 O(n) 搜索算法的查找次数加倍也会导致它花费两倍的时间。 Big-O 表示法确实告诉您算法如何扩展。但这与查找的数量无关,而与您搜索的数据量有关....所以:如果您将 data 的数量翻倍:两个 O(1) 查找应该花费大约相同的时间像之前一样。但是 O(n) 查找的时间应该是以前的两倍。
      【解决方案7】:

      Dictionary is O(1) 检索元素的成本,但这是因为字典是作为哈希表实现的 - 所以您必须首先计算哈希值才能知道要返回哪个元素。哈希表通常效率不高 - 但它们适用于大型数据集或具有大量唯一哈希值的数据集。

      List(除了是一个用来描述数组而不是链表的垃圾词!)会更快,因为它将通过直接计算您想要返回的元素来返回值。

      【讨论】:

        猜你喜欢
        • 2011-03-19
        • 2012-04-05
        • 2023-03-03
        • 2016-08-07
        • 2014-07-21
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-08-05
        相关资源
        最近更新 更多