摘要:為了防止你錯過了之前的文章,以下是鏈接第一部分給開發者的源碼源碼結構第二部分理解內部函數的定義第三部分的變量實現所有的東西都是哈希表基本上,里面的所有東西都是哈希表。哈希后的結果可以被作為正常的數組的鍵值又名為內存塊。表示哈希表的容量。
文章來自:http://www.hoohack.me/2016/02/15/understanding-phps-internal-array-implementation-ch
原文:https://nikic.github.io/2012/03/28/Understanding-PHPs-internal-array-implementation.html
歡迎來到"給PHP開發者的PHP源碼"系列的第四部分,這一部分我們會談論PHP數組在內部是如何表示和在代碼庫里使用的。
為了防止你錯過了之前的文章,以下是鏈接:
第一部分:給PHP開發者的PHP源碼-源碼結構
第二部分:理解PHP內部函數的定義
第三部分:PHP的變量實現
所有的東西都是哈希表基本上,PHP里面的所有東西都是哈希表。不僅僅是在下面的PHP數組實現中,它們還用來存儲對象屬性,方法,函數,變量還有幾乎所有東西。
因為哈希表對PHP來說太基礎了,因此非常值得深入研究它是如何工作的。
那么,什么是哈希表?記住,在C里面,數組是內存塊,你可以通過下標訪問這些內存塊。因此,在C里面的數組只能使用整數且有序的鍵值(那就是說,你不能在鍵值0之后使用1332423442的鍵值)。C里面沒有關聯數組這種東西。
哈希表是這樣的東西:它們使用哈希函數轉換字符串鍵值為正常的整型鍵值。哈希后的結果可以被作為正常的C數組的鍵值(又名為內存塊)。現在的問題是,哈希函數會有沖突,那就是說,多個字符串鍵值可能會生成一樣的哈希值。例如,在PHP,超過64個元素的數組里,字符串"foo"和"oof"擁有一樣的哈希值。
這個問題可以通過存儲可能沖突的值到鏈表中,而不是直接將值存儲到生成的下標里。
HashTable和Bucket那么,現在哈希表的基本概念已經清晰了,讓我們看看在PHP內部中實現的哈希表結構:
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; #endif } HashTable;快速過一下:
nNumOfElements標識現在存儲在數組里面的值的數量。這也是函數count($array)返回的值。
nTableSize表示哈希表的容量。它通常是下一個大于等于nNumOfElements的2的冪值。比如,如果數組存儲了32元素,那么哈希表也是32大小的容量。但如果再多一個元素添加進來,也就是說,數組現在有33個元素,那么哈希表的容量就被調整為64。
這是為了保持哈希表在空間和時間上始終有效。很明顯,如果哈希表太小,那么將會有很多的沖突,而且性能也會降低。另一方面,如果哈希表太大,那么浪費內存。2的冪值是一個很好的折中方案。
nTableMask是哈希表的容量減一。這個mask用來根據當前的表大小調整生成的哈希值。例如,"foo"真正的哈希值(使用DJBX33A哈希函數)是193491849。如果我們現在有64容量的哈希表,我們明顯不能使用它作為數組的下標。取而代之的是通過應用哈希表的mask,然后只取哈希表的低位。
hash | 193491849 | 0b1011100010000111001110001001 & mask | & 63 | & 0b0000000000000000000000111111 --------------------------------------------------------- = index | = 9 | = 0b0000000000000000000000001001
nNextFreeElement是下一個可以使用的數字鍵值,當你使用$array[] = xyz是被使用到。
pInternalPointer 存儲數組當前的位置。這個值在foreach遍歷時可使用reset(),current(),key(),next(),prev()和end()函數訪問。
pListHead和pListTail標識了數組的第一個和最后一個元素的位置。記住:PHP的數組是有序集合。比如,["foo" => "bar", "bar" => "foo"]和["bar" => "foo", "foo" => "bar"]這兩個數組包含了相同的元素,但卻有不同的順序。
arBuckets是我們經常談論的“哈希表(internal C array)”。它用Bucket **來定義,因此它可以被看作數組的bucket指針(我們會馬上談論Bucket是什么)。
pDestructor是值的析構器。如果一個值從HT中移除,那么這個函數會被調用。常見的析構函數是zval_ptr_dtor。zval_ptr_dtor會減少zval的引用數量,而且,如果它遇到o,它會銷毀和釋放它。
最后的四個變量對我們來說不是那么重要。所以簡單地說persistent標識哈希表可以在多個請求里存活,nApplyCount和bApplyProtection防止多次遞歸,inconsistent用來捕獲在調試模式里哈希表的非法使用。
讓我們繼續第二個重要的結構:Bucket:
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;
h是一個哈希值(沒有應用mask值映射之前的值)。
arKey用來保存字符串鍵值。nKeyLength是對應的長度。如果是數字鍵值,那么這兩個變量都不會被使用。
pData及pDataPtr被用來存儲真正的值。對PHP數組來說,它的值是一個zval結構體(但它也在其他地方使用到)。不要糾結為什么有兩個屬性。它們兩者的區別是誰負責釋放值。
pListNext和pListLast標識數組元素的下一個元素和上一個元素。如果PHP想順序遍歷數組它會從pListHead這個bucket開始(在HashTable結構里面),然后使用pListNext bucket作為遍歷指針。在逆序也是一樣,從pListTail指針開始,然后使用pListLast指針作為變量指針。(你可以在用戶代碼里調用end()然后調用prev()函數達到這個效果。)
pNext和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)。
解析完參數后,返回數組就被初始化了:
/* Initialize return array */ 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) { // some code zend_hash_move_forward_ex(Z_ARRVAL_P(keys), &pos); }
這可以很容易地翻譯成PHP代碼:
reset($keys); while (null !== $entry = current($keys)) { // some code 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,而傳遞的是鍵值字符串和它的長度。
符號表"正常的"插入字符串鍵值到哈希表的函數是zend_hash_update,但這里卻使用了zend_symtable_update。它們有什么不同呢?
符號表簡單地說就是哈希表的特殊的類型,這種類型使用在數組里。它跟原始的哈希表不同的是他如何處理數字型的鍵值:在符號表里,"123"和123被看作是相同的。因此,如果你在$array["123"]存儲一個值,你可以在后面使用$array[123]獲取它。
底層可以使用兩種方式實現:要么使用"123"來保存123和"123",要么使用123來保存這兩種鍵值。顯然PHP選擇了后者(因為整型比字符串類型更快和占用更少的空間)。
如果你不小心使用"123"而不是強制轉換為123后插入數據,你會發現符號表一些有趣的事情。一個利用數組到對象的強制轉換如下:
$obj = new stdClass; $obj->{123} = "foo"; $arr = (array) $obj; var_dump($arr[123]); // Undefined offset: 123 var_dump($arr["123"]); // Undefined offset: 123
對象屬性總是使用字符串鍵值來保存,盡管它們是數字。因此$obj->{123} = "foo"這行代碼實際上保存"foo"變量到"123"下標里。當使用數組強制轉換的時候,這個值不會給改變。但當$arr[123]和$arr["123"]都想訪問123下標的值(不是已有的"123"下標)時,都拋出了錯誤。因此,恭喜,你創建了一個隱藏的數組元素。
下一部分下一部分會再次在ircmaxell的博客發表。下一篇會介紹對象和類在內部是如何工作的。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/21338.html
摘要:文章來自原文在給開發者的源碼系列的第三篇文章,我們打算擴展上一篇文章來幫助理解內部是怎么工作的。進入在的核心代碼中,變量被稱為。要轉換一個為值,就調用函數。有了這個東西,我們可以看到函數馬上調用函數。 文章來自:http://www.hoohack.me/2016/02/12/phps-source-code-for-php-developers-part3-variables-ch...
摘要:另一個說明我叫它做宏。你可以為函數定義寫一個宏事實上,就是這么做的,但我們會在后面的文章中深入了解這個。我想說的是,宏允許在預處理編譯時使用更簡單的代碼。或者說頭文件定義了在文件中可以被其他文件看到的函數,包括預處理宏。 文章來自:http://www.hoohack.me/2016/02/04/phps-source-code-for-php-developers-ch 原文:ht...
摘要:文章來自原文歡迎來到給開發者的源碼系列的第二部分。是在內部代表任意一個變量的定義。這種情況下函數會拋出警告,而此函數馬上返回會返回給的用戶層代碼。原因是,是少數通過而不是擴展定義的函數。下一部分下一部分會再次發表在。 文章來自:http://www.hoohack.me/2016/02/10/understanding-phps-internal-function-definitio...
閱讀 2472·2021-11-24 09:39
閱讀 3518·2019-08-30 15:53
閱讀 594·2019-08-29 15:15
閱讀 2903·2019-08-26 13:23
閱讀 3212·2019-08-26 10:48
閱讀 643·2019-08-26 10:31
閱讀 748·2019-08-26 10:30
閱讀 2359·2019-08-23 18:32