C# Dictionary(字典)源碼解析&效率分析


  通過查閱網上相關資料和查看微軟源碼,我對Dictionary有了更深的理解。

  Dictionary,翻譯為中文是字典,通過查看源碼發現,它真的內部結構真的和平時用的字典思想一樣。

  我們平時用的字典主要包括兩個兩個部分,目錄和正文,目錄用來進行第一次的粗略查找,正文進行第二次精確查找。通過將數據進行分組,形成目錄,正文則是分組后的結果。

  而Dictionary對應的是 int[] buckets 和 Entry[] entries,buckets用來記錄要查詢元素分組的起始位置(這么些是為了方便理解,其實是最后一個插入元素的位置沒有元素為-1,查找同組元素通過 entries 元素中的 Next 遍歷,后面會提到),entries記錄所有元素。分組依據是計算元素 Key 的哈希值與 buckets 的長度取余,余數就是分組,指向buckets 位置。通過先查找 buckets 確定元素分組的起始位置,再遍歷分組內元素查找到准確位置。與對應的目錄和正文相同,buckets的 長度大於等於 entries(我理解是為擴展做准備的),buckets 的長度使用 HashHelpers.GetPrime(capacity) 計算,是一個計算得到的最優值。capacity是字典的容量,大於等於字典中實際存儲元素個數。

  Dictionary與真實的字典不同之處在於,真實字典的分組結果的物理位置是連續的,而 Dictionary 不是,他的物理位置順序就是插入的順序,而分組信息記錄在 entries 元素中的 Next 中,Next 是個 int 字段,用來記錄同組元素的下一個位置(若當前為該組第一個插入元素則記錄-1,第一個插入元素在分組遍歷的最后一個)

解析一下Dictionary的幾個關鍵方法

1.Add(Insert 新增&更新方法)

  Add和使用[]更新實際就是調用的Insert,代碼如下。

  首先計算key的哈希值,與buckets取余后確定目錄位置,找到entries的位置,然后通過.next方式遍歷分組內元素,確定是否存在同key元素,再進行新增或更新操作

public void Add(TKey key, TValue value)
{
    Insert(key, value, true);
}
Add
public TValue this[TKey key]
{
    get
    {
        int i = FindEntry(key);
        if (i >= 0) return entries[i].value;
        ThrowHelper.ThrowKeyNotFoundException();
        return default(TValue);
    }
    set
    {
        Insert(key, value, false);
    }
}
索引器
private void Insert(TKey key, TValue value, bool add)
{

    if (key == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
    }

    if (buckets == null) Initialize(0);
    int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
    int targetBucket = hashCode % buckets.Length;

#if FEATURE_RANDOMIZED_STRING_HASHING
            int collisionCount = 0;
#endif

    for (int i = buckets[targetBucket]; i >= 0; i = entries[i].next)
    {
        if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key))
        {
            if (add)
            {
                ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_AddingDuplicate);
            }
            entries[i].value = value;
            version++;
            return;
        }

#if FEATURE_RANDOMIZED_STRING_HASHING
                collisionCount++;
#endif
    }
    int index;
    if (freeCount > 0)
    {
        index = freeList;
        freeList = entries[index].next;
        freeCount--;
    }
    else
    {
        if (count == entries.Length)
        {
            Resize();
            targetBucket = hashCode % buckets.Length;
        }
        index = count;
        count++;
    }

    entries[index].hashCode = hashCode;
    entries[index].next = buckets[targetBucket];
    entries[index].key = key;
    entries[index].value = value;
    buckets[targetBucket] = index;
    version++;

#if FEATURE_RANDOMIZED_STRING_HASHING
 
#if FEATURE_CORECLR
            // In case we hit the collision threshold we'll need to switch to the comparer which is using randomized string hashing
            // in this case will be EqualityComparer<string>.Default.
            // Note, randomized string hashing is turned on by default on coreclr so EqualityComparer<string>.Default will 
            // be using randomized string hashing
 
            if (collisionCount > HashHelpers.HashCollisionThreshold && comparer == NonRandomizedStringEqualityComparer.Default) 
            {
                comparer = (IEqualityComparer<TKey>) EqualityComparer<string>.Default;
                Resize(entries.Length, true);
            }
#else
            if(collisionCount > HashHelpers.HashCollisionThreshold && HashHelpers.IsWellKnownEqualityComparer(comparer)) 
            {
                comparer = (IEqualityComparer<TKey>) HashHelpers.GetRandomizedEqualityComparer(comparer);
                Resize(entries.Length, true);
            }
#endif // FEATURE_CORECLR
 
#endif

}
Insert
private int FindEntry(TKey key) {
            if( key == null) {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
            }
 
            if (buckets != null) {
                int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
                for (int i = buckets[hashCode % buckets.Length]; i >= 0; i = entries[i].next) {
                    if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) return i;
                }
            }
            return -1;
        }
FindEntry

2.Resize(重新調整大小)

  雖然這是個私有方法,但我認為關鍵。它會在元素個數即將超過容量時調用,代碼如下,簡單說明一下。

  該方法會聲明一個 newBuckets 和 newEntrues 用來替換之前的 buckets 和 entrues,聲明后會重構這兩個數組,將 entrues 的值復制到 entrues,重新計算 newBuckets 的值,如果頻繁觸發該方法消耗是較大的,所以創建 Dictionary 時建議指定合理的 capacity(容量)

private void Resize(int newSize, bool forceNewHashCodes)
{
    Contract.Assert(newSize >= entries.Length);
    int[] newBuckets = new int[newSize];
    for (int i = 0; i < newBuckets.Length; i++) newBuckets[i] = -1;
    Entry[] newEntries = new Entry[newSize];
    Array.Copy(entries, 0, newEntries, 0, count);
    if (forceNewHashCodes)
    {
        for (int i = 0; i < count; i++)
        {
            if (newEntries[i].hashCode != -1)
            {
                newEntries[i].hashCode = (comparer.GetHashCode(newEntries[i].key) & 0x7FFFFFFF);
            }
        }
    }
    for (int i = 0; i < count; i++)
    {
        if (newEntries[i].hashCode >= 0)
        {
            int bucket = newEntries[i].hashCode % newSize;
            newEntries[i].next = newBuckets[bucket];
            newBuckets[bucket] = i;
        }
    }
    buckets = newBuckets;
    entries = newEntries;
}
Resize

3.Remove(移除)

  依據key查到元素,將元素賦值為初始值,freeList記錄該位置,下次新增會填充該位置

public bool Remove(TKey key)
{
    if (key == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
    }

    if (buckets != null)
    {
        int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
        int bucket = hashCode % buckets.Length;
        int last = -1;
        for (int i = buckets[bucket]; i >= 0; last = i, i = entries[i].next)
        {
            if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key))
            {
                if (last < 0)
                {
                    buckets[bucket] = entries[i].next;
                }
                else
                {
                    entries[last].next = entries[i].next;
                }
                entries[i].hashCode = -1;
                entries[i].next = freeList;
                entries[i].key = default(TKey);
                entries[i].value = default(TValue);
                freeList = i;
                freeCount++;
                version++;
                return true;
            }
        }
    }
    return false;
}
Remove

   展示一個 Dictionary 實際存儲效果圖

效率對比

新增效率:

  時間ms
Dictionary 0.5643
Array 0.0238
List 0.0853

  這是新增10000個元素操作耗費時間,Dictionary要比Array和List差不多高一個數量級。Array耗時最小是因為最開始就把所有空間申請好了。

查詢效率:

  10 100 1000 10000
Dictionary 0.0056 0.0062 0.0056 0.0079
Array遍歷 0.0022 0.0142 0.1228 1.2396
Array迭代器 0.0574 0.4278 6.1383 82.0934
List遍歷 0.0028 0.0238 0.2588 2.3558
List.Find 0.0654 0.091 0.3384 2.8768
List迭代器 0.0079 0.029 0.2958 3.6625

  可以看出元素在10個時除了Array迭代器和List.Find外,其他的沒有較大差異。Array迭代器耗時的原因是涉及拆箱操作,List.Find可能是Lambda表達式的原因。當元素個數達到100時,Dictionary查詢速度就相對快很多。隨着元素數量增加,Dictionary查詢速度並無太大差異,而其他查詢呈倍數增長。

內存對比(元素個數10000)

  字節差異
Dictionary 350456
Array 40000左右
List 65572

  通過VS查看內存情況(太菜了不知道Array對應的內存怎么看,估計40000左右)

  附測試代碼

using System;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Diagnostics;

namespace ToolBoxTest
{
    /// <summary>
    /// CommonTest 的摘要說明
    /// </summary>
    [TestClass]
    public class CommonTest
    {
        [TestMethod]
        public void Test1()
        {
            Stopwatch sw = new Stopwatch();
            sw.Start();
            List<int> list = new List<int>();
            Dictionary<int, int> dic = new Dictionary<int, int>();
            int count = 10000;
            int key = count / 2;
            int[] arr = new int[count];
            sw.Stop();

            sw.Restart();
            for (int i = 0; i < count; i++)
            {
                arr[i] = i;
            }
            sw.Stop();
            TimeSpan tt1 = sw.Elapsed;

            sw.Restart();
            for (int i = 0; i < count; i++)
            {
                list.Add(i);
            }
            sw.Stop();
            TimeSpan tt2 = sw.Elapsed;

            sw.Restart();
            for (int i = 0; i < count; i++)
            {
                dic.Add(i, i);
            }
            sw.Stop();
            TimeSpan tt3 = sw.Elapsed;

            //字典
            sw.Restart();
            for(int j=0;j<100;j++)
            {
                int index2 = dic[key];
            }
            sw.Stop();
            TimeSpan ts0 = sw.Elapsed;

            //數組 遍歷
            sw.Restart();
            for (int j = 0; j < 100; j++)
            {
                for (int i = 0; i < count; i++)
                    if (arr[i] == key) break;
            }
            sw.Stop();
            TimeSpan ts11 = sw.Elapsed;

            //數組 迭代器
            sw.Restart();
            for (int j = 0; j < 100; j++)
            {
                var p = arr.GetEnumerator();
                while (p.MoveNext())
                {
                    if ((int)p.Current == key)
                        break;
                }
            }
            sw.Stop();
            TimeSpan ts12 = sw.Elapsed;

            //列表 遍歷
            sw.Restart();
            for (int j = 0; j < 100; j++)
            {
                for (int i = 0; i < count; i++)
                    if (list[i] == key) break;
            }
            sw.Stop();
            TimeSpan ts21 = sw.Elapsed;

            //列表 Find
            sw.Restart();
            for (int j = 0; j < 100; j++)
            {
                list.Find(x => x == key);
            }
            sw.Stop();
            TimeSpan ts22 = sw.Elapsed;

            //列表 迭代器
            sw.Restart();
            for (int j = 0; j < 100; j++)
            {
                var q = list.GetEnumerator();
                while (q.MoveNext())
                {
                    if (q.Current == key)
                        break;
                }
            }
            sw.Stop();
            TimeSpan ts23 = sw.Elapsed;
        }
    }
}
測試代碼

總結一下

  Dictionary 和我們日常用到的字典原理是一樣的,通過目錄→正文兩次查找的方式查找元素,是一種空間換時間的方式,查詢效率很高,大多數情況經過2次查詢即可查到(需計算hashCode),但是相應的,開辟了多幾倍的內存空間。另外,新增效率Dictionary明顯較差,所以使用時要分情況而定,查詢>新增(編輯)時優先考慮字典,它的查詢效率真的很高。

使用注意點:

  1.使用前盡量指定合適的容量,字典內元素個數應盡量避免超過容量

  2.查詢時應避免使用 Contains + [] 的方式取值,建議使用 TryGetValue,因為前者實際上是進行了兩次查詢,而后者是一次


 

參考:

https://www.cnblogs.com/zhili/p/DictionaryInDepth.html

https://referencesource.microsoft.com/#mscorlib/system/collections/generic/dictionary.cs


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM