为什么Dictionary.First()这么慢?

时间:2010-06-15 15:50:57

标签: .net performance algorithm hashtable

这不是一个真正的问题,因为我已经找到了答案,但仍然很有趣。

我一直认为如果正确散列,哈希表是最快的关联容器。

但是,以下代码非常慢。它只执行大约100万次迭代,并且在Core 2 CPU上花费的时间超过2分钟。

代码执行以下操作:它维护需要处理的项目集合todo。在每次迭代中,它从该集合中获取一个项目(无关紧要哪个项目),删除它,如果未处理它则处理它(可能添加更多项目进行处理),并重复此项直到没有要处理的项目。

罪魁祸首似乎是Dictionary.Keys.First()操作。

问题是它为何缓慢?

Stopwatch watch = new Stopwatch();
watch.Start();

HashSet<int> processed = new HashSet<int>();
Dictionary<int, int> todo = new Dictionary<int, int>();

todo.Add(1, 1);
int iterations = 0;

int limit = 500000;
while (todo.Count > 0)
{
    iterations++;
    var key = todo.Keys.First();
    var value = todo[key];
    todo.Remove(key);
    if (!processed.Contains(key))
    {
        processed.Add(key);
        // process item here
        if (key < limit) { todo[key + 13] = value + 1; todo[key + 7] = value + 1; }
        // doesn't matter much how
    }
}
Console.WriteLine("Iterations: {0}; Time: {1}.", iterations, watch.Elapsed);

这导致:

Iterations: 923007; Time: 00:02:09.8414388.

简单地将Dictionary更改为SortedDictionary会产生:

Iterations: 499976; Time: 00:00:00.4451514.

只有2倍的迭代次数,速度提高了300倍。

在java中也是如此。 使用HashMap代替DictionarykeySet().iterator().next()代替Keys.First()

5 个答案:

答案 0 :(得分:15)

Dictionary<TKey, TValue>维护一个哈希表。

它的枚举器将循环遍历哈希表中的桶,直到找到非空桶,然后返回该桶中的值。
一旦字典变大,这种操作变得昂贵 此外,从字典中删除项目不会缩小存储区数组,因此在删除项目时First()调用会使更慢。 (因为它必须进一步循环以找到非空桶)

因此,反复调用First()并删除是O(n 2 )。


顺便说一下,你可以避免像这样的值查找:(这不会明显加快速度)

var kvp = todo.First();

//Use kvp.Key and kcp.Value

答案 1 :(得分:4)

Dictionary不会努力跟踪键列表。所以迭代器需要走水桶。许多这些桶,尤其是大型词典,很多都没有。

比较OpenJDK的HashIterator.nextEntryPrivateEntryIterator.nextEntry(使用TreeMap.successor)可能会有所帮助。散列版本遍历未知数量的条目,以查找非空的条目。如果散列表中删除了许多元素(在您的情况下它已经存在),这可能会特别慢。在TreeMap中,我们唯一的步行是我们的有序遍历。方式中没有空值(仅在叶子处)。

答案 2 :(得分:1)

好吧,哈希表没有排序,我的猜测是它必须先进行某种排序才能进行迭代,或者进行某种扫描,如果它已经排序,它可以循环遍历。

答案 3 :(得分:1)

反射器显示Dictionary<TKey, TValue>维护Entry<TKey, TValue>使用的KeyCollection<TKey, TValue>.Enumerator<TKey, TValue>数组。通常,查找应该相对较快,因为它只能索引到数组中(假设您不需要排序First):

// Dictionary<TKey. TValue>
private Entry<TKey, TValue>[] entries;

然而,如果您要移除该数组的第一个元素,那么您最终会走遍数组,直到找到非空数组:

// Dictionary<TKey, TValue>.KeyCollection<TKey, TValue>.Enumerator<TKey, TValue>
while (this.index < this.dictionary.count) {
    if (this.dictionary.entries[this.index].hashCode >= 0) {
        this.currentKey = this.dictionary.entries[this.index].key;
        this.index++;
        return true;
    }
    this.index++;
}

当您删除条目时,您会在entries数组的前面开始获得越来越多的空白,下次检索First的速度会变慢。

答案 4 :(得分:0)

无需查看,排序字典的最简单实现是键的排序列表(如TreeSet)和哈希组合;列表为您提供排序,字典为您提供值。因此钥匙已经可用。 Hashtable没有随时可用的密钥,因此罪魁祸首不是first,而是keys(没有任何证据,可以随意测试假设; D)

相关问题