.NET字典,速度惊人,但它是如何工作的?

时间:2011-03-21 15:31:07

标签: .net memory dictionary performance

好的,我会承认我没有挖出反射器来看看这里发生了什么,但我希望有人可以告诉我。

Microsoft如何快速添加和获取,我可以通过在数组中粘贴项目来快速添加,并且我可以通过对数组进行排序和使用二进制搜索来快速获取。但是,如果我每次添加一个项目以快速获取数据时都要做一个快速输入,那么添加会大大减慢速度,如果每次我尝试获取某些东西时都必须对数据进行排序,那么添加项目会大大减慢。 / p>

有谁知道字典的内部工作原理?它比数组更需要内存,所以除了聪明的算法之外,显然还有其他的东西。

我正在努力理解魔法并从中学习!

5 个答案:

答案 0 :(得分:15)

.Net中的dictionary<T,T>是一个称为哈希表的数据结构:

关于哈希表和.Net词典:

http://en.wikipedia.org/wiki/Hash_table

http://msdn.microsoft.com/en-us/library/4yh14awz.aspx

http://www.cs.auckland.ac.nz/~jmor159/PLDS210/hash_tables.html

关于二进制搜索:

http://en.wikipedia.org/wiki/Binary_search

你是对的,它使用更多的内存而不是数组来检索数据。这是你为更快的访问付出的代价。 (在大多数情况下,当您开始考虑构建哈希表与阵列的设置时间时,情况确实如此,有时排序的数组可能会更快地进行设置时间和访问。通常,这是一个有效的假设。)

答案 1 :(得分:5)

基本原则是:

  1. 设置空数组。
  2. 获取哈希码。
  3. 重新散列哈希以适合数组的大小(例如,如果数组大小为31项,我们可以hash % 31)并将其用作索引。
  4. 然后,检索是以相同的方式查找索引,获取密钥(如果存在的话),并在该项目上调用Equals

    这里显而易见的问题是,如果有两个属于同一索引的项目该怎么办。一种方法是在数组中存储列表或类似内容而不是键值对本身,另一种方法是“重新调用”到不同的索引中。这两种方法都有优点和缺点,Microsoft使用 reprobing 列表。

    超过一定大小,重新调整的数量(或者如果采用该方法,存储列表的大小)变得太大而且近O(1)行为丢失,此时表格被调整大小,以便改善这一点。

    显然,一个非常差的哈希算法可以破坏它,你可以通过构建一个哈希码方法如下的对象字典来证明这一点:

    public override int GetHashCode()
    {
      return 0;
    }
    

    这是有效的,但很可怕,并将你的近O(1)行为变成O(n)(即使O(n)变为坏也是如此。

    还有很多其他细节和优化,但以上是基本原则。

    编辑:

    顺便提一下,如果你有一个完美的哈希值(你知道所有可能的值,并且有一个哈希方法给每个这样的值在一个小范围内给出一个唯一的哈希值),那么就可以避免重复发生的问题 - 更常见的 - 目的哈希表,只是将哈希视为数组的索引。这给出了O(1)行为和最小内存使用,但仅在所有可能值都在较小范围内时适用。

答案 2 :(得分:5)

不久前,我发誓要对这个问题进行详细的回答,这花了我一段时间,因为某些细节和概念对我来说有些生锈,但是在这里:

.NET词典的长度(或种类)如何工作。

让我们从这个概念开始,就像其他许多答案指出的那样,Dictionary<TKey, TValue>hash table的通用实现(就C#语言功能而言)。

哈希表只是一个关联数组,即当您传递一对(键,值)时,该键用于计算哈希码,这将有助于计算哈希表中的位置(称为存储桶)基础存储阵列(称为存储桶),将在其中存储该对和其他一些附加信息。通常,这是通过在数组/存储桶的大小:%上计算哈希码的模hashCode % buckets.Length来实现的。

这种关联数组的搜索,插入和删除操作的平均复杂度为O(1)(即恒定时间)……除非在某些情况下我们稍后会介绍。因此,通常来说,在字典中查找内容要比在列表或数组中查找要快得多,因为您不必“通常”遍历所有值。

如果您一直关注到现在为止所写的内容,那么您会发现可能已经存在问题。如果根据我们的密钥计算出的哈希码与另一个密钥相同,或更糟的是一堆其他密钥,而我们最终位于同一位置怎么办?我们如何管理这些冲突?显然,人们早在几十年前就已经考虑过,并提出了解决冲突的两种主要方法:

  • Separate Chaining:基本上,该对存储在与存储桶(通常称为条目)不同的存储中,例如,对于每个存储桶(计算出的每个索引),我们可以有一个条目列表,其中存储了不同的值已存储在相同的“索引”(由于相同的哈希码),基本上在发生冲突的情况下,您必须遍历键(并找到除哈希码以外的另一种方法来区分它们)
  • Open Addressing:所有内容都存储在存储桶中,并根据我们接下来检查的第一个存储桶,它还以不同的方式来探查值Linear ProbingQuadratic Probing,双重哈希等)

任何一种冲突解决方案的实现有时都可能会有很大不同。对于.NET词典,数据结构依赖于单独链接冲突解决方案,就像我们将看到的几分钟。

现在,让我们看一下如何将内容插入.NET Dictionary<TKey, TValue>中,该内容归纳为以下方法的代码:

private void Insert(TKey key, TValue value, bool add)

注意:阅读下面的插入步骤后,您可以通过检查源代码中作为链接提供的代码来弄清楚删除和查找操作背后的原理。

第1步:给我哈希代码

可以使用两种方法来计算TKey键的哈希码:

  • 如果您不传递任何参数作为IEqualityComparer<TKey>的参数,而该参数基本上是由Dictionary<TKey, TValue>生成的,则它依赖于默认的EqualityComparer<TKey>.Default实现(可用实现{{3 }}),如果TKey的类型不同于自定义类型的所有常用内容(例如基元和字符串),则IEqualityComparer<in TKey>将利用以下实现(包括override):

    • bool Equals(object obj)
    • int GetHashCode()
  • 另一种方法是依靠IEqualityComparer<in TKey>的实现,您可以将其传递给Dictionary<TKey, TValue>构造函数。

here界面如下所示:

// The generic IEqualityComparer interface implements methods to if check two objects are equal
// and generate Hashcode for an object.
// It is use in Dictionary class.  
public interface IEqualityComparer<in T>
{
    bool Equals(T x, T y);
    int GetHashCode(T obj);
}

无论哪种方式,字典都将使用比较器comparer.GetHashCode()

最终具有第一个哈希码。

第2步:获取目标存储段

TKey键到IEqualityComparer<in T>的哈希码有时可能是负数,如果我们想获得数组的正索引,这实际上并没有帮助...

发生的事情是,为了消除负值,将Int32获得的comparer.GetHashCode()哈希码与Int32.MaxValue“与”(即21474836470x7FFFFFFF)(按照布尔逻辑:位):

var hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;

目标存储桶(索引)的获取方式如下:

var targetBucket = hashCode % buckets.Length

稍后还将看到如何调整buckets数组的大小。

bucketsint[])是private的{​​{1}}字段,其中包含Dictionary<TKey, TValue>字段中第一个相关广告位的索引entries,其中Entry[]的定义如下:

Entry

private struct Entry { public int hashCode; public int next; public TKey key; public TValue value; } keyvalue是不言自明的字段,对于hashcode字段,它基本上指示该链中是否还有另一个项目的索引(例如,几个具有相同哈希码的键),如果该条目是链的最后一项,则next字段将设置为next

注意:-1 hashCode中的Entry字段是负值调整后的字段。

第3步:检查是否已有条目

在此阶段,必须注意,行为是不同的,具体取决于您是要更新(struct)还是严格插入(add = false)新值。

我们现在将从第一个条目开始检查与add = true相关的条目:

targetBucket

带有注释的实际(简化)源代码:

var entryIndex = buckets[targetBucket];
var firstEntry = entries[entryIndex];

注意:// Iterating through all the entries related to the targetBucket for (var i = buckets[targetBucket]; i >= 0; i = entries[i].next) { // Checked if all if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) { // If update is not allowed if (add) { // Argument Exception: // "Item with Same Key has already been added" thrown =] ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_AddingDuplicate); } // We update the entry value entries[i].value = value; // Modification while iterating check field version++; return; } } 字段也是在其他常见的.NET数据结构(例如version)中使用的字段,有助于在迭代时(在IEqualityComparer<in T>上)进行检测(并抛出相关的例外)。

第4步:检查是否需要调整阵列大小

List<T>

注意:大约// The entries location in which the data will be inserted var index = 0; // The freeCount field indicates the number of holes / empty slotes available for insertions. // Those available slots are the results of prior removal operations if (freeCount > 0) { // The freeList field points to the first hole (ie. available slot) in the entries index = freeList; freeList = entries[index].next; // The hole is no longer available freeCount--; } else { // The entries array is full // Need to resize it to make it bigger if (count == entries.Length) { Resize(); targetBucket = hashCode % buckets.Length; } index = count; count++; } 通话:

实际上是在7199369方法的早期,新大小的计算如下:

Resize()

第5步:添加条目

由于字典已经检查完孔和大小,因此它最终可以使用计算出的public static int ExpandPrime(int oldSize) { var min = 2 * oldSize; if ((uint) min > 2146435069U && 2146435069 > oldSize) { return 2146435069; } return HashHelpers.GetPrime(min); } hashCodekey和右边的value添加条目刚刚被计算并相应地调整目标铲斗:

index

奖金:字符串特殊待遇

引自下面列出的CodeProject源:

  

为了确保每个存储区的每个“获取”和“添加”操作不会超过100个项目,正在使用碰撞计数器。

     

如果遍历数组以查找或添加项目时,冲突计数器超过100(硬编码限制),并且entries[index].hashCode = hashCode; // If the bucket already contained an item, it will be the next in the collision resolution chain. entries[index].next = buckets[targetBucket]; entries[index].key = key; entries[index].value = value; // The bucket will point to this entry from now on. buckets[targetBucket] = index; // Again, modification while iterating check field version++; 的类型为IEqualityComparer,则新的EqualityComparer<string>.Default正在为备用字符串哈希算法生成实例。

     

如果找到了这样的提供程序,则字典将分配新的数组,并使用新的哈希码和相等提供程序将内容复制到新的数组。

     

对于某些情况下您的字符串键未均匀分布的情况,此优化可能很有用,但也可能导致大量分配并浪费CPU时间来生成新的哈希码(可能是字典中的很多项) 。

用法

每当您使用自定义类型作为键时,别忘了拥有自定义IEqualityComparer<string>或重写两个Object方法(哈希码+等于),以防止自己在插入时感到意外。

不仅可以避免一些意外,而且还可以控制所插入项目的分配。通过具有均匀分布的哈希码,您可以避免链接太多项目,从而避免在这些条目上浪费时间。

针对受访者/人员的注释

我想强调一个事实,那就是知道采访的那些实现细节通常没什么大不了的(实际实现与某些版本的.NET(“ Regular”或Core ...)不同,而且可能仍然可能会有变更))。

如果有人会问我这个问题,我会说:

除非,除非...应该在日常工作哈希表中实现自己,在这种情况下,这类知识(即隐含细节)可能被认为是有帮助的,甚至是必不可少的。

来源:

答案 3 :(得分:3)

它使用hash几乎所有其他字典实现。

答案 4 :(得分:0)

这个问题让我很好奇,所以我写了一篇超快,优化版字典查找快5倍(5倍)比默认的 .NET字典实现

为了简洁起见,我省略了错误检查,但是,添加它是微不足道的。我也没有模仿它让它更容易理解。

它创建了许多嵌套数组,因此查找是通过内存中的对象引用进行链接的问题。它直接导航到内存中的正确对象,而不使用任何描述的循环或散列表。它具有合理的内存效率,因为它只为所需内容分配内存。与散列表不同,无意的桶冲突永远不会有任何问题(当然,除非密钥是相同的)。如果您想自己运行比较,我可以提供完整的测试项目。

/// <summary>
/// Ultra fast dictionary, optimized for retrieval of keys consisting of 3-letter uppercase strings, where each string is 'A' to 'Z'.
/// This is 5 times faster than the default Dictionary<> implementation, but not as flexible.
/// ----start output from tester---
/// Ultra Fast Dictionary.
///   Total time for 2,000,000,000 key retrievals: 19,892 milliseconds. 0.00994600 nanoseconds per retrieval. Sum -1958822656.
/// Normal Dictionary.
///   Total time for 2,000,000,000 key retrievals: 98,397 milliseconds. 0.04919850 nanoseconds per retrieval. Sum -1958822656.
/// ----end output from tester---
/// </summary>
public class DictionaryUltraFast
{
    string[][][] dictionary;

    /// <summary>
    /// Add a string to the dictionary.
    /// </summary>
    public void Add(string key, string value)
    {
        key = key.ToUpper();
        if (dictionary == null)
        {
            dictionary = new string['Z' - 'A' + 1][][];
        }
        if (dictionary[key[0] - 'A'] == null)
        {
            dictionary[key[0] - 'A'] = new string['Z' - 'A' + 1][];
        }
        if (dictionary[key[0] - 'A'][key[1] - 'A'] == null)
        {
            dictionary[key[0] - 'A'][key[1] - 'A'] = new string['Z' - 'A' + 1];
        }
        dictionary[key[0] - 'A'][key[1] - 'A'][key[2] - 'A'] = value;
    }

    public string Get(string key)
    {
        return dictionary[key[0] - 'A'][key[1] - 'A'][key[2] - 'A'];
    }
}