PHP核心中的神器之HashTable
阿新 • • 發佈:2018-12-06
轉載:https://blog.csdn.net/a600423444/article/details/8850617
一、雜湊表定義
雜湊表(或散列表),是將鍵名key按指定的雜湊函式HASH經過HASH(key)計算後對映到表中一個記錄,而這個陣列就是雜湊表。這裡的HASH指任意的函式,例如MD5、CRC32、SHA1或你自定義的函式實現。
二、HashTable效能
HashTable是一種查詢效能極高的資料結構,在很多語言內部都實現了HashTable。理想情況下HashTable的效能是O(1)的,效能消耗主要集中在雜湊函式HASH(key),通過HASH(key)直接定位到表中的記錄。
而在實際情況下經常會發生key1 != key2,但HASH(key1) = HASH(key2),這種情況即Hash碰撞問題,碰撞的概率越低HashTable的效能越好。當然Hash演算法太過複雜也會影響HashTable效能。
三、理解PHP的雜湊表實現
在PHP核心也同樣實現了HashTable並廣泛應用,包括執行緒安全、全域性變數、資源管理等基本上所有的地方都能看到它的身影。不僅如此,在PHP指令碼中陣列(PHP的陣列實質就是HashTable)也是被廣泛使用的,例如陣列形式的配置檔案、資料庫的查詢結果等,可以說是無處不在。
那麼既然PHP的陣列使用率這麼高,內部是如何實現的?它如何解決hash碰撞及實現均勻分佈的?PHP指令碼使用陣列應該注意哪些?
首先通過圖解,大致理解PHP HashTable的實現。 修正:之前認為PHP解決Hahs衝突時,連結串列使用的是單向連結串列。
檢視\Zend\zend_hash.c的zend_hash_move_backwards_ex方法與zend_hash_del_key_or_index方法後,實際上使用的是雙向連結串列
下面通過原始碼來一步一步分析。
1)HashTable在PHP核心的實現
PHP實現HashTable主要是通過兩個資料結構Bucket(桶)和HashTable。從PHP指令碼端來看,HashTable相當於Array物件,而Bucket相當於Array物件裡的某個元素。對於多維陣列實際就是HashTable的某個Bucket裡儲存著另一個HashTable。 HashTable結構:
-
typedef
struct _hashtable {
-
uint nTableSize;
//表長度,並非元素個數
-
uint nTableMask;
//表的掩碼,始終等於nTableSize-1
-
uint nNumOfElements;
//儲存的元素個數
-
ulong nNextFreeElement;
//指向下一個空的元素位置
-
Bucket *pInternalPointer;
//foreach迴圈時,用來記錄當前遍歷到的元素位置
-
Bucket *pListHead;
-
Bucket *pListTail;
-
Bucket **arBuckets;
//儲存的元素陣列
-
dtor_func_t pDestructor;
//解構函式
-
zend_bool persistent;
//是否持久儲存。從這可以發現,PHP陣列是可以實現持久儲存在記憶體中的,而無需每次請求都重新載入。
-
unsigned
char nApplyCount;
-
zend_bool bApplyProtection;
-
} HashTable;
Bucket結構:
-
typedef
struct bucket {
-
ulong h;
//陣列索引
-
uint nKeyLength;
//字串索引的長度
-
void *pData;
//實際資料的儲存地址
-
void *pDataPtr;
//引入的資料儲存地址
-
struct bucket *pListNext;
-
struct bucket *pListLast;
-
struct bucket *pNext;
//雙向連結串列的下一個元素的地址
-
struct bucket *pLast;
//雙向連結串列的下一個元素地址
-
char arKey[
1];
/* Must be last element */
-
} Bucket;
PHP核心雜湊表的雜湊函式很簡單,直接使用 (HashTable->nTableSize & HashTable->nTableMask)的結果作為雜湊函式的實現。這樣做的目的可能也是為了降低Hash演算法的複雜度和提高效能。
1.1)在PHP中初始化一個空陣列時,對應核心中是如何建立HashTable的
$array = new Array();
-
//省略了部分程式碼,提出主要的邏輯
-
ZEND_API
int _zend_hash_init(HashTable *ht, uint nSize,
hash_func_t pHashFunction,
dtor_func_t pDestructor, zend_bool persistent ZEND_FILE_LINE_DC)
-
{
-
uint i =
3;
-
Bucket **tmp;
-
-
SET_INCONSISTENT(HT_OK);
-
-
if (nSize >=
0x80000000) {
//陣列的最大長度是十進位制2147483648
-
/* prevent overflow */
-
ht->nTableSize =
0x80000000;
-
}
else {
-
//陣列的長度是向2的整次冪取圓整
-
//例如陣列的裡有10個元素,那麼實際被分配的HashTable長度是16。100個元素,則被分配128的長度
-
//HashTable的最小長度是8,而非0。因為預設是將1向右移3位,1<<3=8
-
while ((
1U << i) < nSize) {
-
i++;
-
}
-
ht->nTableSize =
1 << i;
-
}
-
-
ht->nTableMask = ht->nTableSize -
1;
-
....
-
-
return SUCCESS;
-
}
從上看出,即使在PHP中初始化一個空陣列或不足8個元素的陣列,都會被建立8個長度的HashTable。同樣建立100個元素的陣列,也會被分配128長度的HashTable。依次類推。
1.2)核心對PHP新增數字索引的處理方式
PHP陣列中,鍵名可以為數字或字串型別。而在核心中只允許數字索引,對於字串索引,核心採用了time33演算法將字串轉換為整型。具體的實現下面會詳細說明。$array[0] = "hello hashtable";
-
//省略了部分程式碼,提出主要的邏輯
-
ZEND_API
int _zend_hash_index_update_or_next_insert(HashTable *ht, ulong h,
void *pData, uint nDataSize,
void **pDest,
int flag ZEND_FILE_LINE_DC)
-
{
-
ulong h;
-
uint nIndex;
-
Bucket *p;
-
//省略了部分程式碼,提出主要的邏輯
-
nIndex = h & ht->nTableMask;
-
p = ht->arBuckets[nIndex];
-
p = (Bucket *) pemalloc_rel(
sizeof(Bucket) -
1, ht->persistent);
-
if (!p) {
-
return FAILURE;
-
}
-
p->nKeyLength =
0;
/* Numeric indices are marked by making the nKeyLength == 0 */
-
p->h = h;
-
INIT_DATA(ht, p, pData, nDataSize);
-
if (pDest) {
-
*pDest = p->pData;
-
}
-
-
ht->arBuckets[nIndex] = p;
-
-
ht->nNumOfElements++;
-
-
return SUCCESS;
-
}
上述也說明了,核心中雜湊表的雜湊函式就是簡單的h & ht->nTableMask,其中h代表PHP中設定的索引號,nTableMask等於雜湊表分配的長度-1。
1.3) 核心對PHP中字串索引的處理方式
$array['index'] = "hello hashtable";
與數字索引相比,只是多了一步將字串轉換為整型。用到的演算法是time33
下面貼出了演算法的實現,就是對字串的每個字元轉換為ASCII碼乘上33並且相加得到的結果。
-
static inline ulong zend_inline_hash_func(const char *arKey, uint nKeyLength)
-
{
-
register ulong hash =
5381;
-
-
/* variant with the hash unrolled eight times */
-
for (; nKeyLength >=
8; nKeyLength -=
8) {
-
hash = ((hash <<
5) + hash) + *arKey++;
-
hash = ((hash <<
5) + hash) + *arKey++;
-
hash = ((hash <<
5) + hash) + *arKey++;
-
hash = ((hash <<
5) + hash) + *arKey++;
-
hash = ((hash <<
5) + hash) + *arKey++;
-
hash = ((hash <<
5) + hash) + *arKey++;
-
hash = ((hash <<
5) + hash) + *arKey++;
-
hash = ((hash <<
5) + hash) + *arKey++;
-
}
-
switch (nKeyLength) {
-
case
7: hash = ((hash <<
5) + hash) + *arKey++;
/* fallthrough... */
-
case
6: hash = ((hash <<
5) + hash) + *arKey++;
/* fallthrough... */
-
case
5: hash = ((hash <<
5) + hash) + *arKey++;
/* fallthrough... */
-
case
4: hash = ((hash <<
5) + hash) + *arKey++;
/* fallthrough... */
-
case
3: hash = ((hash <<
5) + hash) + *arKey++;
/* fallthrough... */
-
case
2: hash = ((hash <<
5) + hash) + *arKey++;
/* fallthrough... */
-
case
1: hash = ((hash <<
5) + hash) + *arKey++;
break;
-
case
0:
break;
-
}
-
return hash;
-
}
-
-
zend_hash.c
-
//下面省略了部分程式碼,提出主要的邏輯
-
ZEND_API
int _zend_hash_add_or_update(HashTable *ht,
const
char *arKey, uint nKeyLength,
void *pData, uint nDataSize,
void **pDest,
int flag ZEND_FILE_LINE_DC)
-
{
-
ulong h;
-
uint nIndex;
-
Bucket *p;
-
-
h = zend_inline_hash_func(arKey, nKeyLength);
//字串轉整型
-
nIndex = h & ht->nTableMask;
-
p = ht->arBuckets[nIndex];
-
p = (Bucket *) pemalloc_rel(
sizeof(Bucket) -
1, ht->persistent);
-
if (!p) {
-
return FAILURE;
-
}
-
p->nKeyLength =
0;
/* Numeric indices are marked by making the nKeyLength == 0 */
-
p->h = h;
-
INIT_DATA(ht, p, pData, nDataSize);
-
if (pDest) {
-
*pDest = p->pData;
-
}
-
-
ht->arBuckets[nIndex] = p;
-
-
ht->nNumOfElements++;
-
-
return SUCCESS;
-
}
2) 核心中如何實現均勻分佈和解決hash碰撞問題的
2.1) 均勻分佈均勻分佈是指,將需要儲存的各個元素均勻的分佈到HashTable中。
而負責計算具體分佈到表中哪個位置的函式就是雜湊函式做的事情,所以雜湊函式的實現直接關係到均勻分佈的效率。
上面也提到了PHP核心中用了簡單的方式實現:h & ht->nTableMask;
2.1)Hash碰撞
Hash碰撞是指,經過Hash演算法後得到的值會出現key1 != key2, 但Hash(key1)卻等於Hash(key2)的情況,這就是碰撞問題。在PHP核心來看,就是會出現key1 != key2, 但key1 & ht->nTableMask卻等於 key2 & ht->nTableMask的情況。
PHP核心使用雙向連結串列的方式來儲存衝突的資料。即Bucket本身也是一個雙向連結串列,當發生衝突時,會將資料按順序向後排列。
如果不發生衝突,Bucket即是長度為1的的雙向連結串列。
-
ZEND_API int zend_hash_find(const HashTable *ht, const char *arKey, uint nKeyLength, void **pData)
-
{
-
ulong h;
-
uint nIndex;
-
Bucket *p;
-
-
IS_CONSISTENT(ht);
-
-
h = zend_inline_hash_func(arKey, nKeyLength);
-
nIndex = h & ht->nTableMask;
-
-
p = ht->arBuckets[nIndex];
-
//找到元素時,並非立即返回,而是要再對比h與nKeyLength,防止hash碰撞。此段程式碼就是遍歷連結串列,直到連結串列尾部。
-
while (p !=
NULL) {
-
if ((p->h == h) && (p->nKeyLength == nKeyLength)) {
-
if (!
memcmp(p->arKey, arKey, nKeyLength)) {
-
*pData = p->pData;
-
return SUCCESS;
-
}
-
}
-
p = p->pNext;
-
}
-
return FAILURE;
-
}
之後,將會寫一篇關於利用Hash演算法,進行分散式儲存的介紹。 原文地址: http://blog.csdn.net/a600423444/article/details/8850617
轉載:https://blog.csdn.net/a600423444/article/details/8850617
一、雜湊表定義
雜湊表(或散列表),是將鍵名key按指定的雜湊函式HASH經過HASH(key)計算後對映到表中一個記錄,而這個陣列就是雜湊表。這裡的HASH指任意的函式,例如MD5、CRC32、SHA1或你自定義的函式實現。
二、HashTable效能
HashTable是一種查詢效能極高的資料結構,在很多語言內部都實現了HashTable。理想情況下HashTable的效能是O(1)的,效能消耗主要集中在雜湊函式HASH(key),通過HASH(key)直接定位到表中的記錄。
而在實際情況下經常會發生key1 != key2,但HASH(key1) = HASH(key2),這種情況即Hash碰撞問題,碰撞的概率越低HashTable的效能越好。當然Hash演算法太過複雜也會影響HashTable效能。
三、理解PHP的雜湊表實現
在PHP核心也同樣實現了HashTable並廣泛應用,包括執行緒安全、全域性變數、資源管理等基本上所有的地方都能看到它的身影。不僅如此,在PHP指令碼中陣列(PHP的陣列實質就是HashTable)也是被廣泛使用的,例如陣列形式的配置檔案、資料庫的查詢結果等,可以說是無處不在。
那麼既然PHP的陣列使用率這麼高,內部是如何實現的?它如何解決hash碰撞及實現均勻分佈的?PHP指令碼使用陣列應該注意哪些?
首先通過圖解,大致理解PHP HashTable的實現。 修正:之前認為PHP解決Hahs衝突時,連結串列使用的是單向連結串列。
檢視\Zend\zend_hash.c的zend_hash_move_backwards_ex方法與zend_hash_del_key_or_index方法後,實際上使用的是雙向連結串列
下面通過原始碼來一步一步分析。
1)HashTable在PHP核心的實現
PHP實現HashTable主要是通過兩個資料結構Bucket(桶)和HashTable。從PHP指令碼端來看,HashTable相當於Array物件,而Bucket相當於Array物件裡的某個元素。對於多維陣列實際就是HashTable的某個Bucket裡儲存著另一個HashTable。 HashTable結構:
-
typedef
struct _hashtable {
-
uint nTableSize;
//表長度,並非元素個數
-
uint nTableMask;
//表的掩碼,始終等於nTableSize-1
-
uint nNumOfElements;
//儲存的元素個數
-
ulong nNextFreeElement;
//指向下一個空的元素位置
-
Bucket *pInternalPointer;
//foreach迴圈時,用來記錄當前遍歷到的元素位置
-
Bucket *pListHead;
-
Bucket *pListTail;
-
Bucket **arBuckets;
//儲存的元素陣列
-
dtor_func_t pDestructor;
//解構函式
-
zend_bool persistent;
//是否持久儲存。從這可以發現,PHP陣列是可以實現持久儲存在記憶體中的,而無需每次請求都重新載入。
-
unsigned
char nApplyCount;
-
zend_bool bApplyProtection;
-
} HashTable;
Bucket結構: