php 數組的結構和定義
數組是PHP中非常強大、靈活的一種數據類型,它的底層實現為散列表(HashTable,也稱作:哈希表)
散列表是根據關鍵碼值(Key value)而直接進行訪問的數據結構,它的key - value之間存在一個映射函數,可以根據key通過映射函數直接索引到對應的value值,它不以關鍵字的比較為基本操作,采用直接尋址技術(就是說,它是直接通過key映射到內存地址上去的),從而加快查找速度,在理想情況下,無須任何比較就可以找到待查關鍵字,查找的期望時間為O(1)。
存放記錄的數組稱做散列表,這個數組用來存儲value,而value具體在數組中的存儲位置由映射函數根據key計算確定,映射函數可以采用取模的方式,key可以通過一些譬如“times 33”的算法得到一個整形值,然後與數組總大小取模得到在散列表中的存儲位置。這是一個普通散列表的實現,PHP散列表的實現整體也是這個思路,只是有幾個特殊的地方,下面就是PHP中HashTable的數據結構:
1 struct _zend_array { 2 zend_refcounted_h gc; //引用計數 3 union { 4 struct { 5 ZEND_ENDIAN_LOHI_4( 6 zend_uchar flags, 7 zend_uchar nApplyCount, 8 zend_uchar nIteratorsCount, 9 zend_uchar consistency)10 } v; 11 uint32_t flags; 12 } u; 13 uint32_t nTableMask; //哈希值計算掩碼,等於nTableSize的負值(nTableMask = -nTableSize) 14 Bucket *arData; //存儲元素數組,指向第一個Bucket 15 uint32_t nNumUsed; //已用Bucket數 16 uint32_t nNumOfElements; //哈希表有效元素數 17 uint32_t nTableSize; //哈希表總大小,為2的n次方 18 uint32_t nInternalPointer; 19 zend_long nNextFreeElement; //下一個可用的數值索引,如:arr[] = 1;arr["a"] = 2;arr[] = 3; 則nNextFreeElement = 2; 20 dtor_func_t pDestructor; 21 };
HashTable中有兩個非常相近的值:nNumUsed
、nNumOfElements
,nNumOfElements
表示哈希表已有元素數,那這個值不跟nNumUsed
一樣嗎?為什麽要定義兩個呢?實際上它們有不同的含義,當將一個元素從哈希表刪除時並不會將對應的Bucket移除,而是將Bucket存儲的zval修改為IS_UNDEF
,只有擴容時發現nNumOfElements與nNumUsed相差達到一定數量(這個數量是:ht->nNumUsed - ht->nNumOfElements > (ht->nNumOfElements >> 5)
)時才會將已刪除的元素全部移除,重新構建哈希表。所以nNumUsed
>=nNumOfElements
HashTable中另外一個非常重要的值arData
,這個值指向存儲元素數組的第一個Bucket,插入元素時按順序 依次插入 數組,比如第一個元素在arData[0]、第二個在arData[1]...arData[nNumUsed]。PHP數組的有序性正是通過arData
保證的,這是第一個與普通散列表實現不同的地方。
既然arData並不是按key映射的散列表,那麽映射函數是如何將key與arData中的value建立映射關系的呢?
實際上這個散列表也在arData
中,比較特別的是散列表在ht->arData內存之前,分配內存時這個散列表與Bucket數組一起分配,arData向後移動到了Bucket數組的起始位置,並不是申請內存的起始位置,這樣散列表可以由arData指針向前移動訪問到,即arData[-1]、arData[-2]、arData[-3]......散列表的結構是uint32_t
,它保存的是value在Bucket數組中的位置。
所以,整體來看HashTable主要依賴arData實現元素的存儲、索引。插入一個元素時先將元素按先後順序插入Bucket數組,位置是idx,再根據key的哈希值映射到散列表中的某個位置nIndex,將idx存入這個位置;查找時先在散列表中映射到nIndex,得到value在Bucket數組的位置idx,再從Bucket數組中取出元素。
映射函數(即:散列函數)是散列表的關鍵部分,它將key與value建立映射關系,一般映射函數可以根據key的哈希值與Bucket數組大小取模得到,即key->h % ht->nTableSize
,但是PHP卻不是這麽做的:
nIndex = key->h | ht->nTableMask;
顯然位運算要比取模更快。
nTableMask
為nTableSize
的負數,即:nTableMask = -nTableSize
,因為nTableSize
等於2^n,所以nTableMask
二進制位右側全部為0,也就保證了nIndex落在數組索引的範圍之內(|nIndex| <= nTableSize
):
哈希碰撞是指不同的key可能計算得到相同的哈希值(數值索引的哈希值直接就是數值本身),但是這些值又需要插入同一個散列表。一般解決方法是將Bucket串成鏈表,查找時遍歷鏈表比較key。
PHP的實現也是如此,只是將鏈表的指針指向轉化為了數值指向,即:指向沖突元素的指針並沒有直接存在Bucket中,而是保存到了value的zval
中:
1 struct _zval_struct { 2 zend_value value; /* value */ 3 ... 4 union { 5 uint32_t var_flags; 6 uint32_t next; /* hash collision chain(哈希碰撞鏈) */ 7 uint32_t cache_slot; /* literal cache slot */ 8 uint32_t lineno; /* line number (for ast nodes) */ 9 uint32_t num_args; /* arguments number for EX(This) */ 10 uint32_t fe_pos; /* foreach position */ 11 uint32_t fe_iter_idx; /* foreach iterator index */ 12 } u2; 13 };
當出現沖突時將原value的位置保存到新value的zval.u2.next
中,然後將新插入的value的位置更新到散列表,也就是後面沖突的value始終插入header
數組中存儲元素的結構
1 typedef struct _Bucket { 2 zval val; //存儲的具體value,這裏嵌入了一個zval,而不是一個指針 3 zend_ulong h; //key根據times 33計算得到的哈希值,或者是數值索引編號 4 zend_string *key; //存儲元素的key 5 } Bucket;
php 數組的結構和定義