PHP源碼之數組的內部實現

哈希表

基本上,PHP裏面的所有東西都是哈希表。不僅僅是在下面的php數組實現中,它們還用來存儲對象屬性,方法,函數,變量還有幾乎所有東西。

因爲哈希表對PHP來說太基礎了,因此非常值得深入研究它是如何工作的。

什麼是哈希表

記住,在C裏面,數組是內存塊,你可以通過下標訪問這些內存塊。因此,在C裏面的數組只能使用整數且有序的鍵值(那就是說,你不能在鍵值0之後使用1332423442的鍵值)。C裏面沒有關聯數組這種東西。

哈希表是這樣的東西:它們使用哈希函數轉換字符串鍵值爲正常的整型鍵值。哈希後的結果可以被作爲正常的C數組的鍵值(又名爲內存塊)。現在的問題是,哈希函數會有衝突,那就是說,多個字符串鍵值可能會生成一樣的哈希值。例如,在PHP,超過64個元素的數組裏,字符串”foo”和”oof”擁有一樣的哈希值。

這個問題可以通過存儲可能衝突的值到鏈表中,而不是直接將值存儲到生成的下標裏。

HashTable和Bucket

typedef struct _hashtable {
uint nTableSize;
uint nTableMask;
uint nNumOfElements;
ulong nNextFreeElement;
Bucket pInternalPointer;
Bucket 
pListHead;
Bucket pListTail;
Bucket *
arBuckets;
dtor_func_t pDestructor;
zend_bool persistent;
unsigned char nApplyCount;
zend_bool bApplyProtection;
if ZEND_DEBUG int inconsistent;
} HashTable;

  1. nNumOfElements
    標識現在存儲在數組裏面的值的數量。這也是函數count的返回值
  2. nTableSize
    表示哈希表的容量。它通常是下一個大於等於nNumOfElements的2的冪值。比如,如果數組存儲了32元素,那麼哈希表也是32大小的容量。但如果再多一個元素添加進來,也就是說,數組現在有33個元素,那麼哈希表的容量就被調整爲64。 這是爲了保持哈希表在空間和時間上始終有效。很明顯,如果哈希表太小,那麼將會有很多的衝突,而且性能也會降低。另一方面,如果哈希表太大,那麼浪費內存。2的冪值是一個很好的折中方案。
  3. nTableMask
    是哈希表的容量減一。這個mask用來根據當前的表大小調整生成的哈希值。例如,”foo”真正的哈希值(使用DJBX33A哈希函數)是193491849。如果我們現在有64容量的哈希表,我們明顯不能使用它作爲數組的下標。取而代之的是通過應用哈希表的mask,然後只取哈希表的低位。
    hash | 193491849 | 0b1011100010000111001110001001
    & mask | & 63 | & 0b0000000000000000000000111111
    = index | = 9 | = 0b0000000000000000000000001001
  4. nNextFreeElement
    是下一個可以使用的數字鍵值,當你使用$array[] = xyz是被使用到。
  5. pInternalPointer
    存儲數組當前的位置。這個值在foreach遍歷時可使用reset(),current(),key(),next(),prev()和end()函數訪問。
  6. pListHead和pListTail
    標識了數組的第一個和最後一個元素的位置。記住:PHP的數組是有序集合。比如,[‘foo’ => ‘bar’, ‘bar’ => ‘foo’]和[‘bar’ => ‘foo’, ‘foo’ => ‘bar’]這兩個數組包含了相同的元素,但卻有不同的順序。
  7. arBuckets
    是我們經常談論的“哈希表(internal C array)”。它用Bucket **來定義,因此它可以被看作數組的bucket指針(我們會馬上談論Bucket是什麼)。
  8. pDestructor
    是值的析構器。如果一個值從HT中移除,那麼這個函數會被調用。常見的析構函數是zval_ptr_dtor。zval_ptr_dtor會減少zval的引用數量,而且,如果它遇到o,它會銷燬和釋放它。

typedef struct bucket {
ulong h;
uint nKeyLength;
void pData;
void 
pDataPtr;
struct bucket pListNext;
struct bucket 
pListLast;
struct bucket pNext;
struct bucket 
pLast;
const char *arKey;
} Bucket;

  1. h
    是一個哈希值(沒有應用mask值映射之前的值)。

  2. arKey
    用來保存字符串鍵值。

  3. nKeyLength
    是對應的長度。如果是數字鍵值,那麼這兩個變量都不會被使用。

  4. pData

  5. pDataPtr
    被用來存儲真正的值。對PHP數組來說,它的值是一個zval結構體(但它也在其他地方使用到)。不要糾結爲什麼有兩個屬性。它們兩者的區別是誰負責釋放值。

  6. pListNext

  7. pListLast
    標識數組元素的下一個元素和上一個元素。如果PHP想順序遍歷數組它會從pListHead這個bucket開始(在HashTable結構裏面),然後使用pListNext bucket作爲遍歷指針。在逆序也是一樣,從pListTail指針開始,然後使用pListLast指針作爲變量指針。(你可以在用戶代碼裏調用end()然後調用prev()函數達到這個效果。)

  8. pNext

  9. pLast
    生成我上面提到的“可能衝突的值鏈表”。arBucket數組存儲第一個可能值的bucket。如果該bucket沒有正確的鍵值,PHP會查找pNext指向的bucket。它會一直指向後面的bucket直到找到正確的bucket。pLast在逆序中也是一樣的原理。

你可以看到,PHP的哈希表實現相當複雜。這是它使用超靈活的數組類型要付出的代價。

哈希表是怎麼被使用的?

Zend Engine定義了大量的API函數供哈希表使用。低級的哈希表函數預覽可以在
zend_hash.h文件裏面找到。另外Zend Engine在zend_API.h文件定義了稍微高級一些的API。

我們沒有足夠的時間去講所有的函數,但是我們至少可以查看一些實例函數,看看它是如何工作的。我們將使用array_fill_keys作爲實例函數。

使用第二部分提到的技巧你可以很容易地找到函數在
ext/standard/array.c文件裏面定義了。現在,讓我們來快速查看這個函數。
跟大部分函數一樣,函數的頂部有一堆變量的定義,然後調用zend_parse_parameters
函數:

zval keys, val, **entry;
HashPosition pos;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "az", &keys, &val) == FAILURE) {
return;
}

很明顯,az參數說明第一個參數類型是數組(即變量keys),第二個參數是任意的zval(即變量val)。

解析完參數後,返回數組就被初始化了:

array_init_size(return_value,zend_hash_num_elements(Z_ARRVAL_P(keys));

這一行包含了array API裏面存在的三步重要的部分:

  1. Z_ARRVAL_P宏從zval裏面提取值到哈希表。

  2. zend_hash_num_elements提取哈希表元素的個數(nNumOfElements屬性)。

  3. array_init_size使用size變量初始化數組。

因此,這一行使用與鍵值數組一樣大小來初始化數組到return_value變量裏。

這裏的size只是一種優化方案。函數也可以只調用
array_init(return_value),這樣隨着越來越多的元素添加到數組裏,PHP就會多次重置數組的大小。通過指定特定的大小,PHP會在一開始就分配正確的內存空間。
數組被初始化並返回後,函數用跟下面大致相同的代碼結構,使用while循環變量keys數組:

zend_hash_internal_pointer_reset_ex(Z_ARRVAL_P(keys), &pos);
while (zend_hash_get_current_data_ex(Z_ARRVAL_P(keys), (void **)&entry, &pos) == SUCCESS) {
zend_hash_move_forward_ex(Z_ARRVAL_P(keys), &pos);
}

這可以很容易地翻譯成PHP代碼:

reset($keys);
while (null !== $entry = current($keys)) {
next($keys);
}

跟下面的一樣:

foreach ($keys as $entry) {
// some code
}

唯一不同的是,C的遍歷並沒有使用內部的數組指針,而使用它自己的pos變量來存儲當前的位置。

在循環裏面的代碼分爲兩個分支:一個是給數字鍵值,另一個是其他鍵值。數字鍵值的分支只有下面的兩行代碼:

zval_add_ref(&val);
zend_hash_index_update(Z_ARRVAL_P(return_value),
Z_LVAL_PP(entry), &val,
sizeof(zval *), NULL);

這看起來太直接了:首先值的引用增加了(添加值到哈希表意味着增加另一個指向它的引用),然後值被插入到哈希表中。zend_hash_index_update宏的參數分別是,需要更新的哈希表Z_ARRVAL_P(return_value),整型下標
Z_LVAL_PP(entry),值&val,值的大小sizeof(zval *)以及目標指針(這個我們不關注,因此是NULL)。

非數字下標的分支就稍微複雜一點:

zval key, key_ptr = entry;
if (Z_TYPE_PP(entry) != IS_STRING) {
key = *entry;
zval_copy_ctor(&key);
convert_to_string(&key);
key_ptr = &key;
}
zval_add_ref(&val);
zend_symtable_update(Z_ARRVAL_P(return_value), Z_STRVAL_P(key_ptr), Z_STRLEN_P(key_ptr) + 1, &val, sizeof(zval 
), NULL);
if (key_ptr != *entry) {
zval_dtor(&key);
}

首先,使用convert_to_string將鍵值轉換爲字符串(除非它已經是字符串了)。在這之前,entry被複制到新的key變量。key = **entry這一行實現。另外,
zval_copy_ctor函數會被調用,不然複雜的結構(比如字符串或數組)不會被正確地複製。

上面的複製操作非常有必要,因爲要保證類型轉換不會改變原來的數組。如果沒有copy操作,強制轉換不僅僅修改局部的變量,而且也修改了在鍵值數組中的值(顯然,這對用戶來說非常意外)。

顯然,循環結束之後,複製操作需要再次被移除,zval_dtor(&key)
做的就是這個工作。zval_ptr_dtor和zval_dtor的不同是zval_ptr_dtor只會在refcount變量爲0時銷燬zval變量,而zval_dtor會馬上銷燬它,而不是依賴
refcount的值。這就爲什麼你看到zval_pte_dtor使用”normal”變量而zval_dtor
使用臨時變量,這些臨時變量不會在其他地方使用。而且,zval_ptr_dtor
會在銷燬之後釋放zval的內容而zval_dtor不會。因爲我們沒有malloc()任何東西,因此我們也不需要free(),因此在這方面,zval_dtor做了正確的選擇。

現在來看看剩下的兩行(重要的兩行^^):

zval_add_ref(&val);
zend_symtable_update(Z_ARRVAL_P(return_value), Z_STRVAL_P(key_ptr), Z_STRLEN_P(key_ptr) + 1, &val, sizeof(zval *), NULL);

這跟數字鍵值分支完成後的操作非常相似。不同的是,現在調用的是
zend_symtable_update而不是zend_hash_index_update,而傳遞的是鍵值字符串和它的長度。

轉載地址:http://blog.csdn.net/u010412301/article/details/53983452

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章