C# Hashtable原始碼剖析
原始碼版本為 .NET Framework 4.6.1
本系列持續更新,敬請關注
有投入,有產出。
Hashtable實現一個雜湊表(也叫散列表),將鍵對映到相應的值。任何非 null 物件都可以用作鍵。
雜湊表的實現比較複雜,最好先了解一下相關的方法和概念。
(注:非基礎性,主要涉及Hashtable的實現原理)
水平有限,若有不對之處,望指正。
雜湊表的概念
雜湊表是根據關鍵碼值進行訪問的資料結構,它是通過把關鍵碼值對映到表中對應的一個位置來訪問記錄值,以加快查詢速度(給定表M,存在函式f(key),對任意給定的關鍵字值key,代入函式後若能得到包含該關鍵字的記錄在表中的地址,則稱表M為雜湊(Hash)表,函式f(key)為雜湊(Hash) 函式。)。
什麼是Hash
Hash,一般翻譯做“雜湊”,也有直接音譯為“雜湊”的,就是把任意長度的輸入(又叫做預對映),通過雜湊演算法,變換成固定長度的輸出,該輸出就是雜湊值。這種轉換是一種壓縮對映,也就是,雜湊值的空間通常遠小於輸入的空間,不同的輸入可能會雜湊成相同的輸出,而不可能從雜湊值來唯一的確定輸入值。簡單的說就是一種將任意長度的訊息壓縮到某一固定長度的訊息摘要的函式。
雜湊衝突
我們使用一個下標範圍比較大的陣列來儲存元素。可以設計一個函式(雜湊函式, 也叫做雜湊函式),使得每個元素的關鍵字都與一個函式值(即陣列下標)相對應,於是用這個陣列單元來儲存這個元素。
雜湊函式的目標是儘量減少衝突,但實際應用中衝突是無法避免的,所以在衝突發生時,必須有相應的解決方案。而發生衝突的可能性又跟以下兩個因素有關:
1. 裝填因子α:所謂裝填因子是指合希表中已存入的記錄數n與雜湊地址空間大小m的比值,即 α=n / m ,α越小,衝突發生的可能性就越小;α越大,衝突發生的可能性就越大(α取值範圍0.1f ~ 1.0f)。這很容易理解,因為α越小,雜湊表中空閒單元的比例就越大,所以待插入記錄同已插入的記錄發生衝突的可能性就越小;反之,α越大,雜湊表中空閒單元的比例就越小,所以待插入記錄同已插入記錄衝突的可能性就越大;另一方面,α越小,儲存桶的利用率就越低;反之,儲存桶的利用率就越高。為了既兼顧減少衝突的發生,又兼顧提高儲存空間的利用率,通常把α控制在0.6~0.9的範圍之內,C#的HashTable類把α的值定為0.72。 2. 與所採用的雜湊函式有關。若雜湊函式選擇得當,就可使雜湊地址儘可能均勻地分佈在雜湊地址空間上,從而減少衝突的發生;否則,就可能使雜湊地址集中於某些區域,從而加大沖突發生的可能性。
雜湊衝突解決
衝突解決技術可分為兩大類:開雜湊法(又稱為鏈地址法)和閉雜湊法(又稱為開放地址法)。雜湊表是用陣列實現的一片連續的地址空間,兩種衝突解決技術的區別在於發生衝突的元素是儲存在這片陣列的空間之外還是空間之內(一個數組空間或多個數組空間):
1. 開雜湊法發生衝突的元素儲存於陣列空間之外。可以把“開”字理解為需要另外“開闢”空間儲存發生衝突的元素。
2. 閉雜湊法發生衝突的元素儲存於陣列空間之內。可以把“閉”字理解為所有元素,不管是否有衝突,都“關閉”於陣列之中。閉雜湊法又稱開放地址法,意指陣列空間對所有元素,不管是否衝突都是開放的。
閉雜湊法(開放地址法)
閉雜湊法是把所有的元素儲存在雜湊表陣列中。當發生衝突時,在衝突位置的附近尋找可存放記錄的空單元。尋找“下一個”空位的過程稱為探測。上述方法可用如下公式表示:
hi=( h(key) + di ) % m i=1,2,…,k (k≤m-1)
其中h(key)為雜湊函式;m為雜湊表長;di為增量的序列。根據di取值的不同,可以分成幾種探測方法,下面介紹的是Hashtable所使用到的雙重雜湊法。
雙重雜湊法(DoubleHashing)
雙重雜湊法是經典的資料表結構(T)。設 n 為儲存在 T 中元素的數目,m為T的容量,則T的載入因子為α= n / m, α:1 > α >0。
它是以關鍵字的另一個雜湊函式值作為增量。設兩個雜湊函式為:h_1 和 h_2,則得到的探測序列為:
h(i,k) = ( h_1(k) + i * h_2(k) ) % m,m為雜湊表的容量,i: 1 < i < m - 1。
定義h_2的方法較多,但無採用什麼方法都必須使h_2(k)的值和m互素(又稱互質,表示兩數的最大公約數為1,或者說是兩數沒有共同的因子,1除外)才能使發生衝突的同義詞地址均勻地分佈在整個雜湊表中,否則可能造成同義詞地址的迴圈計算。若m為素數,則h_2取1至m-1之間的任何數均與m互素。
Hashtable的實現
Hashtable實現了IDictionary,在名稱空間System.Collections中,表示根據鍵的雜湊程式碼進行組織的鍵/值對的集合。
- 基本成員
internal const Int32 HashPrime = 101;
private const Int32 InitialSize = 3;
private struct bucket {
public Object key;//鍵
public Object val;//值
public int hash_col;//雜湊碼
}
private bucket[] buckets;
private int count;//元素總數
private int occupancy;//衝突次數
private int loadsize;
private float loadFactor;
private volatile int version;
private volatile bool isWriterInProgress;
private ICollection keys;
private ICollection values;
HashPrime:是一個固定的素數;
InitialSize :是雜湊表的預設容量;
count :記錄雜湊表中的元素總數;
occupancy: 記錄雜湊表發生衝突的次數;
loadsize: 裝載容量值,相當於一個閾值,達到了這個數值,將對雜湊表進行擴容;
loadFactor: 雜湊表中的元素佔有資料桶空間的一個比率,這個比例直接決定了雜湊表在什麼時候進行擴容;
buckets:稱為資料桶,用於儲存雜湊表中的元素,它是一個結構體,包含:
1. key:鍵,鍵是不能重複的;
2. val:值,可以是任何的型別(想要型別安全可以選擇Dictionary,是Hashtable的泛型實現);
3. hash_col:是一個Int32型別,它的最高位是符號位,為“0”時,表示這是一個正整數;為“1”時表示負整數。hash_coll使用最高位表示當前位置是否發生衝突,正數表示未發生衝突;負數表示當前位置存在衝突。之所以專門使用一個位用於存放雜湊碼並標註是否發生衝突,主要是為了提高雜湊表的執行效率。
- 雜湊函式
在Hashtable中的兩個雜湊函式分別為:
1. h_1(k) = k.GetHashCode():第一個雜湊函式直接用預設的GetHashCode()方法;
2. h_2(k) = (1 + ((h_1(k) * HashPrime) % (hashsize - 1))):HashPrime為私有成員101的素數,hashsize為雜湊表長度。之所以會進行取模運算是為了保證結果值的範圍在[0, hashsize - 1]。
建構函式
Hashtable的建構函式很多,這裡記錄一個最核心的建構函式
Hashtable(Int32, Single):使用指定的初始容量、指定的載入因子、預設的雜湊程式碼提供程式和預設比較器來初始化 Hashtable 類的新的空例項。有兩個主要引數:
1. capacity:最初可包含的元素的近似數目。
2. loadFactor:0.1 到 1.0 範圍內的數字,再乘以提供最佳效能的預設值0.72f。結果是元素與儲存桶的最大比率,建議該值使用預設的1.0f,因為該值越小,越會造成空間的浪費。
public Hashtable(int capacity, float loadFactor){
if (capacity < 0)
throw new ArgumentOutOfRangeException("capacity",
Environment.GetResourceString("ArgumentOutOfRange_NeedNonNegNum"));
if (!(loadFactor >= 0.1f && loadFactor <= 1.0f))
throw new ArgumentOutOfRangeException("loadFactor",
Environment.GetResourceString("ArgumentOutOfRange_HashtableLoadFactor", .1, 1.0));
Contract.EndContractBlock();
//官方的備註是 0.72f為最優載入因子
this.loadFactor = 0.72f * loadFactor;
//原始容量
double rawsize = capacity / this.loadFactor;
//容量不超過 Int32.MaxValue
if (rawsize > Int32.MaxValue)
throw new ArgumentException(Environment.GetResourceString("Arg_HTCapacityOverflow"));
//容量大於等於預設容量值
int hashsize = (rawsize > InitialSize) ? HashHelpers.GetPrime((int)rawsize) : InitialSize;
//資料桶,是HashTable內部維護的關鍵資料
buckets = new bucket[hashsize];
//裝載容量,結果為小於或等於容量的值
loadsize = (int)(this.loadFactor * hashsize);
isWriterInProgress = false;
Contract.Assert( loadsize < hashsize, "Invalid hashtable loadsize!");
}
構建雜湊演算法的函式
函式裡包含了雙重雜湊法的雜湊函式和增量
private uint InitHash(Object key, int hashsize, out uint seed, out uint incr) {
//取正數值,第一和雜湊函式h_1(k)
uint hashcode = (uint) GetHash(key) & 0x7FFFFFFF;
seed = (uint) hashcode;
//第二個雜湊函式h_2(k)的增量
incr = (uint)(1 + ((seed * HashPrime) % ((uint)hashsize - 1)));
return hashcode;
}
新增元素
Add(Object, Object):將帶有指定鍵和值的元素新增到 Hashtable 中。
emptySlotNumber : 記錄第一個定址到的可用插槽。
在Add(Object, Object)中,有一個do while迴圈,用於元素的插入驗證。思路分析:
- 尋找對映到的插槽是否可用插槽(空插槽:衝突空插槽和正常空插槽),若定址到可用衝突插槽則記錄,若定址到有正常空插槽,表示衝突鏈結束,將元素插入第一個定址到的可用插槽(若無記錄,將元素插入當前的正常空插槽中)。
- 若對映不到可用插槽,對比現有插槽中的雜湊碼(hash_coll)和鍵(key)是否已經存在,若是,則丟擲異常。
- 若插槽已滿,判斷是否有可用的衝突插槽,將其插入。
public virtual void Add(Object key, Object value) {
Insert(key, value, true);
}
private void Insert (Object key, Object nvalue, bool add) {
if (key == null) {
throw new ArgumentNullException("key", Environment.GetResourceString("ArgumentNull_Key"));
}
Contract.EndContractBlock();
if (count >= loadsize) {
//當元素的總數大於等於裝載量時,自動擴容
expand();
}
else if(occupancy > loadsize && count > 100) {
//在元素總數大於100之後,判斷衝突計數大於裝載量時,將HashTable重新雜湊
rehash();
}
uint seed;
uint incr;
uint hashcode = InitHash(key, buckets.Length, out seed, out incr);
int ntry = 0;//定址次數,不得大於等於雜湊表容量
int emptySlotNumber = -1; //用於記錄第一個定址到的可用插槽
int bucketNumber = (int) (seed % (uint)buckets.Length);
do {
//有衝突的空插槽
if (emptySlotNumber == -1 && (buckets[bucketNumber].key == buckets) && (buckets[bucketNumber].hash_coll < 0))
emptySlotNumber = bucketNumber;
//正常的空插槽
if ((buckets[bucketNumber].key == null) ||
(buckets[bucketNumber].key == buckets && ((buckets[bucketNumber].hash_coll & unchecked(0x80000000))==0))) {
//將元素放入定址到的第一個可用插槽
if (emptySlotNumber != -1)
bucketNumber = emptySlotNumber;
Thread.BeginCriticalRegion();
isWriterInProgress = true;
buckets[bucketNumber].val = nvalue;
buckets[bucketNumber].key = key;
buckets[bucketNumber].hash_coll |= (int) hashcode;
count++;
UpdateVersion();
isWriterInProgress = false;
Thread.EndCriticalRegion();
if(ntry > HashHelpers.HashCollisionThreshold && HashHelpers.IsWellKnownEqualityComparer(_keycomparer))
{
if(_keycomparer == null || !(_keycomparer is System.Collections.Generic.RandomizedObjectEqualityComparer))
{
_keycomparer = HashHelpers.GetRandomizedEqualityComparer(_keycomparer);
rehash(buckets.Length, true);
}
}
return;
}
//替換更新(此處值變更,Update操作),若新增重複的鍵,則丟擲異常
if (((buckets[bucketNumber].hash_coll & 0x7FFFFFFF) == hashcode) &&
KeyEquals (buckets[bucketNumber].key, key)) {
if (add) {
throw new ArgumentException(Environment.GetResourceString("Argument_AddingDuplicate__", buckets[bucketNumber].key, key));
}
Thread.BeginCriticalRegion();
isWriterInProgress = true;
buckets[bucketNumber].val = nvalue;
UpdateVersion();
isWriterInProgress = false;
Thread.EndCriticalRegion();
if(ntry > HashHelpers.HashCollisionThreshold && HashHelpers.IsWellKnownEqualityComparer(_keycomparer))
{
if(_keycomparer == null || !(_keycomparer is System.Collections.Generic.RandomizedObjectEqualityComparer))
{
_keycomparer = HashHelpers.GetRandomizedEqualityComparer(_keycomparer);
rehash(buckets.Length, true);
}
}
return;
}
//存在衝突 將雜湊值設定為負數
if (emptySlotNumber == -1) {
if( buckets[bucketNumber].hash_coll >= 0 ) {
buckets[bucketNumber].hash_coll |= unchecked((int)0x80000000);
occupancy++;
}
}
bucketNumber = (int) (((long)bucketNumber + incr)% (uint)buckets.Length);
//定址次數肯定是不能超過最大索引下標的,此處迴圈用於衝突的二次定址
} while (++ntry < buckets.Length);
//插槽已滿,將元素插入第一個定址到的可用插槽
if (emptySlotNumber != -1)
{
Thread.BeginCriticalRegion();
isWriterInProgress = true;
buckets[emptySlotNumber].val = nvalue;
buckets[emptySlotNumber].key = key;
buckets[emptySlotNumber].hash_coll |= (int) hashcode;
count++;
UpdateVersion();
isWriterInProgress = false;
Thread.EndCriticalRegion();
if(buckets.Length > HashHelpers.HashCollisionThreshold && HashHelpers.IsWellKnownEqualityComparer(_keycomparer))
{
if(_keycomparer == null || !(_keycomparer is System.Collections.Generic.RandomizedObjectEqualityComparer))
{
_keycomparer = HashHelpers.GetRandomizedEqualityComparer(_keycomparer);
rehash(buckets.Length, true);
}
}
return;
}
Contract.Assert(false, "hash table insert failed! Load factor too high, or our double hashing function is incorrect.");
throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_HashInsertFailed"));
}
獲取值 / 設定值
索引器:Hashtable中通過索引器來進行獲取值 / 設定值。
雜湊表的讀的操作有三個步驟 ︰
(1) 計算雜湊值和找到的插槽號。
(2) 比較雜湊碼,如果相等,請轉至步驟 3。否則讀失敗,結束。
(3) 比較關鍵字,如果相等,返回包含在儲存桶中的值。否則讀失敗,結束。
在索引器的原始碼中 有兩個do while迴圈,最外面的迴圈用於遍歷衝突鏈,巢狀的迴圈用於防止資料讀髒。
public virtual Object this[Object key] {
get {
if (key == null) {
throw new ArgumentNullException("key", Environment.GetResourceString("ArgumentNull_Key"));
}
Contract.EndContractBlock();
uint seed;
uint incr;
//生成一個數據桶的結構副本,防止其他執行緒同一時間對同一個結構進行調整。
bucket[] lbuckets = buckets;
uint hashcode = InitHash(key, lbuckets.Length, out seed, out incr);
int ntry = 0;
bucket b;
int bucketNumber = (int) (seed % (uint)lbuckets.Length);
do
{
int currentversion;
int spinCount = 0;
do {
currentversion = version;
b = lbuckets[bucketNumber];
//這裡使用執行緒休眠是為了防止資源爭奪而導致CPU過度消耗
if( (++spinCount) % 8 == 0 ) {
Thread.Sleep(1);
}
//若有其他執行緒在做調整,等待完成再獲取最新的值
} while ( isWriterInProgress || (currentversion != version) );
if (b.key == null) {
return null;
}
if (((b.hash_coll & 0x7FFFFFFF) == hashcode) &&
KeyEquals (b.key, key))
return b.val;
bucketNumber = (int) (((long)bucketNumber + incr)% (uint)lbuckets.Length);
} while (b.hash_coll < 0 && ++ntry < lbuckets.Length);
return null;
}
set {
//更新現在的鍵值
Insert(key, value, false);
}
}
//比較函式
protected virtual bool KeyEquals(Object item, Object key)
{
Contract.Assert(key != null, "key can't be null here!");
if( Object.ReferenceEquals(buckets, item)) {
return false;
}
if (Object.ReferenceEquals(item,key))
return true;
if (_keycomparer != null)
return _keycomparer.Equals(item, key);
return item == null ? false : item.Equals(key);
}
移除元素
Remove(Object):從 Hashtable 中移除帶有指定鍵的元素。
Hashtable刪除元素 分兩種情況處理:
(1) 正常插槽,將key賦空引用,hash_col賦值0。
(2)衝突插槽,將key指向buckets資料桶,將hash_col賦值-2147483648 (同時賦值key和hash_col是為了與雜湊碼為0的衝突插槽區分開)。
public virtual void Remove(Object key) {
if (key == null) {
throw new ArgumentNullException("key", Environment.GetResourceString("ArgumentNull_Key"));
}
Contract.EndContractBlock();
Contract.Assert(!isWriterInProgress, "Race condition detected in usages of Hashtable - multiple threads appear to be writing to a Hashtable instance simultaneously! Don't do that - use Hashtable.Synchronized.");
uint seed;
uint incr;
uint hashcode = InitHash(key, buckets.Length, out seed, out incr);
int ntry = 0;
bucket b;
int bn = (int) (seed % (uint)buckets.Length);
//第一次迴圈若找不到值,那麼表示有衝突鏈 或 鍵值不存在
do {
b = buckets[bn];
if (((b.hash_coll & 0x7FFFFFFF) == hashcode) && KeyEquals (b.key, key)) {
Thread.BeginCriticalRegion();
isWriterInProgress = true;
//正常插槽雜湊碼為0 / 衝突插槽雜湊碼為負數
buckets[bn].hash_coll &= unchecked((int)0x80000000);
if (buckets[bn].hash_coll != 0) {
//衝突插槽的key指向buckets
buckets[bn].key = buckets;
}
else {
//正常插槽的key賦空引用
buckets[bn].key = null;
}
buckets[bn].val = null;
count--;
UpdateVersion();
isWriterInProgress = false;
Thread.EndCriticalRegion();
return;
}
bn = (int) (((long)bn + incr)% (uint)buckets.Length);
//迴圈衝突鏈
} while (b.hash_coll < 0 && ++ntry < buckets.Length);
}
最後
載入因子確定元素與儲存桶的最大比率。較小的載入因素會導致更快地平均查詢時間,但這樣做會增加的記憶體消耗。預設的載入因子 1.0 通常提供速度和大小之間的最佳平衡。
隨著雜湊表的元素數量不斷增加,實際的裝載因子(裝載量)也會隨著增大。當雜湊表中元素的數量達到裝載因子的數值時,雜湊表的容量將自動增加到大於當前數的兩倍的最小質數。