本篇文章帶大家來比較一下PHP5和PHP7的陣列實作方式,看看它們之間的差異!
從 PHP 5 到 PHP 7 ,PHP 透過對 hashtable 資料結構和實作方式的修改,使得陣列在記憶體佔用和效能上有了很大的提升。
⒈ 資料結構
// PHP 5 中 hashtable 的数据结构定义 typedef struct bucket { ulong h; /*对于索引数组,存储 key 的原始值;对于关联数组,存储 key 的 hash 之后的值*/ uint nKeyLength; /*关联数组时存储 key 的长度,索引数组此值为 0*/ void *pData; /*指向数组 value 的地址*/ void *pDataPtr; /*如果 value 为指针,则由 pDataPtr 记录 vlaue,pData 则指向 pDataPtr*/ // PHP 5 中数组元素的顺序是固定的,无论什么时候遍历,数组元素总是与插入时的顺序一致 // PHP 5 中使用双向链表来保证数组元素的顺序,pListNext 和 pListLast 分别按照 // 元素插入顺序记录当前 bucket 的下一个和上一个 bucket struct bucket *pListNext; struct bucket *pListLast; // PHP 5 使用拉链法解决 hash 碰撞,pNext 和 pLast 分别存储当前 bucket // 在冲突的双向链表中的下一个和上一个相邻的 bucket struct bucket *pNext; struct bucket *pLast; const char *arKey; /*关联数组是存储 key 的原始值*/ } Bucket; typedef struct _hashtable { uint nTableSize; /*当前 ht 所分配的 bucket 的总数,2^n*/ uint nTableMask; /*nTableSize - 1,用于计算索引*/ uint nNumOfElements; /*实际存储的元素的数量*/ ulong nNextFreeElement; /*下一个可以被使用的整数 key*/ Bucket *pInternalPointer; /*数组遍历时,记录当前 bucket 的地址*/ Bucket *pListHead; Bucket *pListTail; Bucket **arBuckets; /*记录 bucket 的 C 语言数组*/ dtor_func_t pDestructor; /*删除数组元素时内部调用的函数*/ zend_bool persistent; /*标识 ht 是否永久有效*/ unsigned char nApplyCount; /*ht 允许的最大递归深度*/ zend_bool bApplyProtection; /*是否启用递归保护*/ #if ZEND_DEBUG int inconsistent; #endif } HashTable; // PHP 7 中 hashtable 的数据结构 // PHP 7 中个子版本以及阶段版本中对 hashtable 的数据结构的定义会有微小的差别,这里使用的是 PHP 7.4.0 中的定义 struct _zend_string { zend_refcounted_h gc; zend_ulong h; /*字符串 key 的 hash 值*/ size_t len; /*字符串 key 的长度*/ char val[1]; /*存储字符串的值,利用了 struct hack*/ }; typedef struct _Bucket { zval val; /*内嵌 zval 结构,存储数组的 value 值*/ zend_ulong h; /* hash value (or numeric index) */ zend_string *key; /* string key or NULL for numerics */ } Bucket; typedef struct _zend_array HashTable; struct _zend_array { zend_refcounted_h gc; union { struct { ZEND_ENDIAN_LOHI_4( zend_uchar flags, zend_uchar _unused, zend_uchar nIteratorsCount, zend_uchar _unused2) } v; uint32_t flags; } u; uint32_t nTableMask; /*作用与 PHP 5 中 hashtable 中 nTableMask 作用相同,但实现逻辑稍有变化*/ Bucket *arData; /*存储 bucket 相关的信息*/ uint32_t nNumUsed; /*ht 中已经使用的 bucket 的数量,在 nNumOfElements 的基础上加上删除的 key*/ uint32_t nNumOfElements; uint32_t nTableSize; uint32_t nInternalPointer; zend_long nNextFreeElement; dtor_func_t pDestructor; };
不考慮其他開銷,單從Bucket 所佔用的空間來看:在PHP 5 中,考慮到記憶體對齊,一個Bucket
佔用的空間為72 位元組;在PHP 7 中,一個zend_value
佔8 個位元組,一個zval
佔16 位元組,一個Bucket
佔32 個位元組。相較之下,PHP 7 中 Bucket
的記憶體空間消耗比 PHP 5 低了一半以上。
具體PHP 5 數組的記憶體消耗情況,之前的文章已有講解,這裡不再贅述
現在來談談Bucket
的儲存:在PHP 5 中,arBucket
是一個C 語言數組,長度為nTableSize
,儲存的是指向Bucket
的指針,發生hash
碰撞的Bucket
以雙向鍊錶的方式連接。
在PHP 7 中,Bucket
依照陣列元素寫入的順序依序存儲,其索引值為idx
,該值儲存在*arData
左側的映射區域中。 idx
在映射區域中的索引為nIndex
,nIndex
值為負數,由陣列key
的hash
值以nTableMask
進行或運算得到。
// nTableMask 为 -2 倍的 nTableSize 的无符号表示 #define HT_SIZE_TO_MASK(nTableSize) \ ((uint32_t)(-((nTableSize) + (nTableSize)))) // 在通过 idx 查找 Bucket 时,data 默认为 Bucket 类型,加 idx 表示向右偏移 idx 个 Bucket 位置 # define HT_HASH_TO_BUCKET_EX(data, idx) \ ((data) + (idx)) // 在通过 nIndex 查找 idx 时, // (uint32_t*)(data) 首先将 data 转换成了 uint32_t* 类型的数组 // 然后将 nIndex 转换成有符号数(负数),然后以数组的方式查找 idx 的值 #define HT_HASH_EX(data, idx) \ ((uint32_t*)(data))[(int32_t)(idx)] nIndex = h | ht->nTableMask; idx = HT_HASH_EX(arData, nIndex); p = HT_HASH_TO_BUCKET_EX(arData, idx);
這裡需要指出,nTableMask
之所以設定為nTableSize
的兩倍,是這樣在計算nIndex
時可以減少hash
碰撞的機率。
⒉ 新增/修改元素
- #PHP 5
先來談談PHP 5 中數組元素的添加和修改,由於PHP 5 中數組元素的插入順序以及hash 碰撞都是透過雙向鍊錶的方式來維護,所以雖然實現起來有些複雜,但理解起來相對容易一些。
// hash 碰撞双向链表的维护 #define CONNECT_TO_BUCKET_DLLIST(element, list_head) \ (element)->pNext = (list_head); \ (element)->pLast = NULL; \ if ((element)->pNext) { \ (element)->pNext->pLast = (element); \ } #define CONNECT_TO_GLOBAL_DLLIST_EX(element, ht, last, next)\ (element)->pListLast = (last); \ (element)->pListNext = (next); \ if ((last) != NULL) { \ (last)->pListNext = (element); \ } else { \ (ht)->pListHead = (element); \ } \ if ((next) != NULL) { \ (next)->pListLast = (element); \ } else { \ (ht)->pListTail = (element); \ } \ // 数组元素插入顺序双向链表的维护 #define CONNECT_TO_GLOBAL_DLLIST(element, ht) \ CONNECT_TO_GLOBAL_DLLIST_EX(element, ht, (ht)->pListTail, (Bucket *) NULL); \ if ((ht)->pInternalPointer == NULL) { \ (ht)->pInternalPointer = (element); \ } // 数组元素的更新 #define UPDATE_DATA(ht, p, pData, nDataSize) \ if (nDataSize == sizeof(void*)) { \ // 值为指针类型的元素的更新 \ if ((p)->pData != &(p)->pDataPtr) { \ pefree_rel((p)->pData, (ht)->persistent); \ } \ // pDataPtr 存储元素值的地址,pData 存储 pDataPtr 的地址 \ memcpy(&(p)->pDataPtr, pData, sizeof(void *)); \ (p)->pData = &(p)->pDataPtr; \ } else { \ // 如果数组元素为值类型,则存入 pData,此时 pDataPtr 为 Null \ if ((p)->pData == &(p)->pDataPtr) { \ (p)->pData = (void *) pemalloc_rel(nDataSize, (ht)->persistent); \ (p)->pDataPtr=NULL; \ } else { \ (p)->pData = (void *) perealloc_rel((p)->pData, nDataSize, (ht)->persistent); \ /* (p)->pDataPtr is already NULL so no need to initialize it */ \ } \ memcpy((p)->pData, pData, nDataSize); \ } // 数组元素的初始化 #define INIT_DATA(ht, p, _pData, nDataSize); \ if (nDataSize == sizeof(void*)) { \ // 指针类型元素的初始化 \ memcpy(&(p)->pDataPtr, (_pData), sizeof(void *)); \ (p)->pData = &(p)->pDataPtr; \ } else { \ // 值类型元素的初始化 \ (p)->pData = (void *) pemalloc_rel(nDataSize, (ht)->persistent);\ memcpy((p)->pData, (_pData), nDataSize); \ (p)->pDataPtr=NULL; \ } // hashtable 初始化校验,如果没有初始化,则初始化 hashtable #define CHECK_INIT(ht) do { \ if (UNEXPECTED((ht)->nTableMask == 0)) { \ (ht)->arBuckets = (Bucket **) pecalloc((ht)->nTableSize, sizeof(Bucket *), (ht)->persistent); \ (ht)->nTableMask = (ht)->nTableSize - 1; \ } \ } while (0) // 数组元素的新增或更新(精简掉了一些宏调用和代码片段) 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; CHECK_INIT(ht); h = zend_inline_hash_func(arKey, nKeyLength); nIndex = h & ht->nTableMask; p = ht->arBuckets[nIndex]; while (p != NULL) { if (p->arKey == arKey || ((p->h == h) && (p->nKeyLength == nKeyLength) && !memcmp(p->arKey, arKey, nKeyLength))) { // 数组元素更新逻辑 if (flag & HASH_ADD) { return FAILURE; } ZEND_ASSERT(p->pData != pData); if (ht->pDestructor) { ht->pDestructor(p->pData); } UPDATE_DATA(ht, p, pData, nDataSize); if (pDest) { *pDest = p->pData; } return SUCCESS; } p = p->pNext; } // 数组元素新增逻辑 if (IS_INTERNED(arKey)) { p = (Bucket *) pemalloc(sizeof(Bucket), ht->persistent); p->arKey = arKey; } else { p = (Bucket *) pemalloc(sizeof(Bucket) + nKeyLength, ht->persistent); p->arKey = (const char*)(p + 1); memcpy((char*)p->arKey, arKey, nKeyLength); } p->nKeyLength = nKeyLength; INIT_DATA(ht, p, pData, nDataSize); p->h = h; // hash 碰撞链表维护 CONNECT_TO_BUCKET_DLLIST(p, ht->arBuckets[nIndex]); if (pDest) { *pDest = p->pData; } // 数组元素写入顺序维护 CONNECT_TO_GLOBAL_DLLIST(p, ht); ht->arBuckets[nIndex] = p; ht->nNumOfElements++; ZEND_HASH_IF_FULL_DO_RESIZE(ht); /* If the Hash table is full, resize it */ return SUCCESS; }
PHP 5 中的陣列在新增或修改元素時,首先會根據給定的key
計算得到對應的hash 值,然後據此得到arBuckets
的索引nIndex
,最後得到鍊錶中第一個Bucket
( hash
碰撞鍊錶的表頭),即p
。
如果是更新數組中已有的項,那麼會從p
開始遍歷hash
碰撞鍊錶,直到找到arkey
與給定的key
相同的Bucket
,然後更新pData
。
如果是向數組中新增項,首先會判斷給定的key
是否為interned string
類型,如果是,那麼只需要為 Bucket
申請內存,然後將p->arKey
指向給定的key
的地址即可,否則在為新的Bucket
申請內存的同時還需要為給定的key
申請內存,然後將p->arKey
指向為key
申請的內存的地址。之後會對新申請的 Bucket 進行初始化,最後要做的兩件事:維護 hash
碰撞鍊錶和陣列元素寫入順序鍊錶。在維護hash 碰撞的鍊錶時,新增的Bucket 是放在鍊錶頭的位置;維護數組元素寫入順序的鍊錶時,新增的Bucket
是放在鍊錶的末尾,同時將hashtable
的pListTail
指向新增的Bucket
。
關於PHP 中的interned string,之前在講解PHP 7 對字串處理邏輯最佳化的時候已經說明,這裡不再贅述
- PHP 7
PHP 7 在hashtable
的資料結構上做了比較大的改動,同時放棄了使用雙向鍊錶的方式來維護hash
碰撞和陣列元素的寫入順序,在記憶體管理以及效能上得到了提升,但理解起來卻不如PHP 5 中的實作方式直觀。
#define Z_NEXT(zval) (zval).u2.next #define HT_HASH_EX(data, idx) \ ((uint32_t*)(data))[(int32_t)(idx)] # define HT_IDX_TO_HASH(idx) \ ((idx) * sizeof(Bucket)) // PHP 7 中数组添加/修改元素(精简了部分代码) static zend_always_inline zval *_zend_hash_add_or_update_i(HashTable *ht, zend_string *key, zval *pData, uint32_t flag) { zend_ulong h; uint32_t nIndex; uint32_t idx; Bucket *p, *arData; /*... ...*/ ZEND_HASH_IF_FULL_DO_RESIZE(ht); /* If the Hash table is full, resize it */ add_to_hash: idx = ht->nNumUsed++; ht->nNumOfElements++; arData = ht->arData; p = arData + idx; p->key = key; p->h = h = ZSTR_H(key); nIndex = h | ht->nTableMask; Z_NEXT(p->val) = HT_HASH_EX(arData, nIndex); HT_HASH_EX(arData, nIndex) = HT_IDX_TO_HASH(idx); ZVAL_COPY_VALUE(&p->val, pData); return &p->val; }
這裡需要先說明 nNumUsed
和 nNumOfElements
的差異:
按图中示例,此时 nNumUsed
的值应该为 5,但 nNumOfElements
的值则应该为 3。在 PHP 7 中,数组元素按照写入顺序依次存储,而 nNumUsed
正好可以用来充当数组元素存储位置索引的功能。
另外就是 p = arData + idx
,前面已经讲过 arData
为 Bucket
类型,这里 +idx
意为指针从 arData
的位置开始向右偏移 idx
个 Bucket
的位置。宏调用 HT_HASH_EX
也是同样的道理。
最后就是 Z_NEXT(p->val)
,PHP 7 中的 Bucket
结构都内嵌了一个 zval
,zval
中的联合体 u2
中有一项 next
用来记录hash
碰撞的信息。nIndex
用来标识 idx
在映射表中的位置,在往 hashtable
中新增元素时,如果根据给定的 key
计算得到的 nIndex
的位置已经有值(即发生了 hash
碰撞),那么此时需要将 nIndex
所指向的位置的原值记录到新增的元素所对应的 Bucket
下的 val.u2.next
中。宏调用 HT_IDX_TO_HASH
的作用是根据 idx
计算得到 Bucket 的以字节为单位的偏移量。
⒊ 删除元素
- PHP 5
在 PHP 5 中,数组元素的删除过程中的主要工作是维护 hash 碰撞链表和数组元素写入顺序的链表。
// 删除 Bucket 的代码(精简了部分代码片段) static zend_always_inline void i_zend_hash_bucket_delete(HashTable *ht, Bucket *p) { if (p->pLast) { p->pLast->pNext = p->pNext; } else { ht->arBuckets[p->h & ht->nTableMask] = p->pNext; } if (p->pNext) { p->pNext->pLast = p->pLast; } if (p->pListLast != NULL) { p->pListLast->pListNext = p->pListNext; } else { /* Deleting the head of the list */ ht->pListHead = p->pListNext; } if (p->pListNext != NULL) { p->pListNext->pListLast = p->pListLast; } else { /* Deleting the tail of the list */ ht->pListTail = p->pListLast; } if (ht->pInternalPointer == p) { ht->pInternalPointer = p->pListNext; } ht->nNumOfElements--; if (ht->pDestructor) { ht->pDestructor(p->pData); } if (p->pData != &p->pDataPtr) { pefree(p->pData, ht->persistent); } pefree(p, ht->persistent); } // 元素删除 ZEND_API int zend_hash_del_key_or_index(HashTable *ht, const char *arKey, uint nKeyLength, ulong h, int flag) { uint nIndex; Bucket *p; if (flag == HASH_DEL_KEY) { h = zend_inline_hash_func(arKey, nKeyLength); } nIndex = h & ht->nTableMask; p = ht->arBuckets[nIndex]; while (p != NULL) { if ((p->h == h) && (p->nKeyLength == nKeyLength) && ((p->nKeyLength == 0) /* Numeric index (short circuits the memcmp() check) */ || !memcmp(p->arKey, arKey, nKeyLength))) { /* String index */ i_zend_hash_bucket_delete(ht, p); return SUCCESS; } p = p->pNext; } return FAILURE; }
PHP 5 中数组在删除元素时,仍然是先根据给定的 key
计算 hash
,然后找到 arBucket
的 nIndex
,最终找到需要删除的 Bucket
所在的 hash
碰撞的链表,通过遍历链表,找到最终需要删除的 Bucket
。
在实际删除 Bucket 的过程中,主要做的就是维护两个链表:hash 碰撞链表和数组元素写入顺序链表。再就是释放内存。
- PHP 7
由于 PHP 7 记录 hash 碰撞信息的方式发生了变化,所以在删除元素时处理 hash 碰撞链表的逻辑也会有所不同。另外,在删除元素时,还有可能会遇到空间回收的情况。
#define IS_UNDEF 0 #define Z_TYPE_INFO(zval) (zval).u1.type_info #define Z_TYPE_INFO_P(zval_p) Z_TYPE_INFO(*(zval_p)) #define ZVAL_UNDEF(z) do { \ Z_TYPE_INFO_P(z) = IS_UNDEF; \ } while (0) static zend_always_inline void _zend_hash_del_el_ex(HashTable *ht, uint32_t idx, Bucket *p, Bucket *prev) { // 从 hash 碰撞链表中删除指定的 Bucket if (!(HT_FLAGS(ht) & HASH_FLAG_PACKED)) { if (prev) { Z_NEXT(prev->val) = Z_NEXT(p->val); } else { HT_HASH(ht, p->h | ht->nTableMask) = Z_NEXT(p->val); } } idx = HT_HASH_TO_IDX(idx); ht->nNumOfElements--; if (ht->nInternalPointer == idx || UNEXPECTED(HT_HAS_ITERATORS(ht))) { // 如果当前 hashtable 的内部指针指向了要删除的 Bucket 或当前 hashtable 有遍历 // 操作,那么需要避开当前正在被删除的 Bucket uint32_t new_idx; new_idx = idx; while (1) { new_idx++; if (new_idx >= ht->nNumUsed) { break; } else if (Z_TYPE(ht->arData[new_idx].val) != IS_UNDEF) { break; } } if (ht->nInternalPointer == idx) { ht->nInternalPointer = new_idx; } zend_hash_iterators_update(ht, idx, new_idx); } if (ht->nNumUsed - 1 == idx) { //如果被删除的 Bucket 在数组的末尾,则同时回收与 Bucket 相邻的已经被删除的 Bucket 的空间 do { ht->nNumUsed--; } while (ht->nNumUsed > 0 && (UNEXPECTED(Z_TYPE(ht->arData[ht->nNumUsed-1].val) == IS_UNDEF))); ht->nInternalPointer = MIN(ht->nInternalPointer, ht->nNumUsed); } if (p->key) { // 删除 string 类型的索引 zend_string_release(p->key); } // 删除 Bucket if (ht->pDestructor) { zval tmp; ZVAL_COPY_VALUE(&tmp, &p->val); ZVAL_UNDEF(&p->val); ht->pDestructor(&tmp); } else { ZVAL_UNDEF(&p->val); } } static zend_always_inline void _zend_hash_del_el(HashTable *ht, uint32_t idx, Bucket *p) { Bucket *prev = NULL; if (!(HT_FLAGS(ht) & HASH_FLAG_PACKED)) { // 如果被删除的 Bucket 存在 hash 碰撞的情况,那么需要找出其在 hash 碰撞链表中的位置 uint32_t nIndex = p->h | ht->nTableMask; uint32_t i = HT_HASH(ht, nIndex); if (i != idx) { prev = HT_HASH_TO_BUCKET(ht, i); while (Z_NEXT(prev->val) != idx) { i = Z_NEXT(prev->val); prev = HT_HASH_TO_BUCKET(ht, i); } } } _zend_hash_del_el_ex(ht, idx, p, prev); } ZEND_API void ZEND_FASTCALL zend_hash_del_bucket(HashTable *ht, Bucket *p) { IS_CONSISTENT(ht); HT_ASSERT_RC1(ht); _zend_hash_del_el(ht, HT_IDX_TO_HASH(p - ht->arData), p); }
PHP 7 中数组元素的删除,其最终目的是删除指定的 Bucket
。在删除 Bucket 时还需要处理好 hash
碰撞链表维护的问题。由于 PHP 7 中 hash
碰撞只维护了一个单向链表(通过 Bucket.val.u2.next
来维护),所以在删除 Bucket
时还需要找出 hash
碰撞链表中的前一项 prev
。最后,在删除 Bucket 时如果当前的 hashtable 的内部指针(nInternalPointer
)正好指向了要删除的 Bucket
或存在遍历操作,那么需要改变内部指针的指向,同时在遍历时跳过要删除的 Bucket
。另外需要指出的是,并不是每一次删除 Bucket
的操作都会回收相应的内存空间,通常删除 Bucket
只是将其中 val
的类型标记为 IS_UNDEF
,只有在扩容或要删除的 Bucket
为最后一项并且相邻的 Bucket
为 IS_UNDEF
时才会回收其内存空间。
⒋ 数组遍历
- PHP 5
由于 PHP 5 中有专门用来记录数组元素写入顺序的双向链表,所以数组的遍历逻辑相对比较简单。
// 数组的正向遍历 ZEND_API int zend_hash_move_forward_ex(HashTable *ht, HashPosition *pos) { HashPosition *current = pos ? pos : &ht->pInternalPointer; IS_CONSISTENT(ht); if (*current) { *current = (*current)->pListNext; return SUCCESS; } else return FAILURE; } // 数组的反向遍历 ZEND_API int zend_hash_move_backwards_ex(HashTable *ht, HashPosition *pos) { HashPosition *current = pos ? pos : &ht->pInternalPointer; IS_CONSISTENT(ht); if (*current) { *current = (*current)->pListLast; return SUCCESS; } else return FAILURE; }
  PHP 5 中 hashtable 的数据结构中有三个字段:pInternalPointer
用来记录数组遍历过程中指针指向的当前 Bucket
的地址;pListHead
用来记录保存数组元素写入顺序的双向链表的表头;pListTail
用来记录保存数组元素写入顺序的双向链表的表尾。数组的正向遍历从 pListHead
的位置开始,通过不断更新 pInternalPointer
来实现;反向遍历从 pListTail
开始,通过不断更新 pInternalPointer
来实现。
- PHP 7
由于 PHP 7 中数组的元素是按照写入的顺序存储,所以遍历的逻辑相对简单,只是在遍历过程中需要跳过被标记为 IS_UNDEF
的项。
⒌ hash 碰撞
- PHP 5
前面在谈论数组元素添加/修改的时候已有提及,每次在数组新增元素时,都会检查并处理 hash
碰撞,即 CONNECT_TO_BUCKET_DLLIST
,代码如下
CONNECT_TO_BUCKET_DLLIST(p, ht->arBuckets[nIndex]); #define CONNECT_TO_BUCKET_DLLIST(element, list_head) \ (element)->pNext = (list_head); \ (element)->pLast = NULL; \ if ((element)->pNext) { \ (element)->pNext->pLast = (element); \ }
在新增元素时,如果当前 arBuckets
的位置没有其他元素,那么只需要直接写入新增的 Bucket
即可,否则新增的 Bucket
会被写入 hash
碰撞双向链表的表头位置。
- PHP 7
前面已经讲过,PHP 7 中的 hashtable
是通过 Bucket
中的 val.u2.next
项来维护 hash
碰撞的单向链表的。所以,在往 hashtable
中添加新的元素时,最后需要先将 nIndex
位置的值写入新增的 Bucket
的 val.u2.next
中。而在删除 Bucket
时,需要同时找出要删除的 Bucket
所在的 hash
碰撞链表中的前一项,以便后续的 hash
碰撞链表的维护。
⒍ 扩容
- PHP 5
在数组元素新增/修改的 API 中的最后有一行代码 ZEND_HASH_IF_FULL_DO_RESIZE(ht)
来判断当前 hashtable
是否需要扩容,如果需要则对其进行扩容。
// 判断当前 hashtable 是否需要扩容 #define ZEND_HASH_IF_FULL_DO_RESIZE(ht) \ if ((ht)->nNumOfElements > (ht)->nTableSize) { \ zend_hash_do_resize(ht); \ } // hashtable 扩容(精简部分代码) ZEND_API int zend_hash_do_resize(HashTable *ht) { Bucket **t; if ((ht->nTableSize << 1) > 0) { /* Let's double the table size */ t = (Bucket **) perealloc(ht->arBuckets, (ht->nTableSize << 1) * sizeof(Bucket *), ht->persistent); ht->arBuckets = t; ht->nTableSize = (ht->nTableSize << 1); ht->nTableMask = ht->nTableSize - 1; zend_hash_rehash(ht); } } // 扩容后对 hashtable 中的元素进行 rehash(精简部分代码) ZEND_API int zend_hash_rehash(HashTable *ht) { Bucket *p; uint nIndex; if (UNEXPECTED(ht->nNumOfElements == 0)) { return SUCCESS; } memset(ht->arBuckets, 0, ht->nTableSize * sizeof(Bucket *)); for (p = ht->pListHead; p != NULL; p = p->pListNext) { nIndex = p->h & ht->nTableMask; CONNECT_TO_BUCKET_DLLIST(p, ht->arBuckets[nIndex]); ht->arBuckets[nIndex] = p; } return SUCCESS; }
首先,PHP 5 hashtable
扩容的前提条件:数组中元素的数量超过 hashtable
的 nTableSize
的值。之后,hashtable
的 nTableSize
会翻倍,然后重新为 arBuckets
分配内存空间并且更新 nTableMask
的值。最后,由于 nTableMask
发生变化,需要根据数组元素的索引重新计算 nIndex
,然后将之前的 Bucket
关联到新分配的 arBuckets
中新的位置。
- PHP 7
在 PHP 7 的新增/修改 hashtable 的 API 中也有判断是否需要扩容的代码 ZEND_HASH_IF_FULL_DO_RESIZE(ht)
,当满足条件时则会执行扩容操作。
#define HT_SIZE_TO_MASK(nTableSize) \ ((uint32_t)(-((nTableSize) + (nTableSize)))) #define HT_HASH_SIZE(nTableMask) \ (((size_t)(uint32_t)-(int32_t)(nTableMask)) * sizeof(uint32_t)) #define HT_DATA_SIZE(nTableSize) \ ((size_t)(nTableSize) * sizeof(Bucket)) #define HT_SIZE_EX(nTableSize, nTableMask) \ (HT_DATA_SIZE((nTableSize)) + HT_HASH_SIZE((nTableMask))) #define HT_SET_DATA_ADDR(ht, ptr) do { \ (ht)->arData = (Bucket*)(((char*)(ptr)) + HT_HASH_SIZE((ht)->nTableMask)); \ } while (0) #define HT_GET_DATA_ADDR(ht) \ ((char*)((ht)->arData) - HT_HASH_SIZE((ht)->nTableMask)) // 当 hashtable 的 nNumUsed 大于或等于 nTableSize 时则执行扩容操作 #define ZEND_HASH_IF_FULL_DO_RESIZE(ht) \ if ((ht)->nNumUsed >= (ht)->nTableSize) { \ zend_hash_do_resize(ht); \ } # define HT_HASH_RESET(ht) \ memset(&HT_HASH(ht, (ht)->nTableMask), HT_INVALID_IDX, HT_HASH_SIZE((ht)->nTableMask)) #define HT_IS_WITHOUT_HOLES(ht) \ ((ht)->nNumUsed == (ht)->nNumOfElements) // 扩容(精简部分代码) static void ZEND_FASTCALL zend_hash_do_resize(HashTable *ht) { if (ht->nNumUsed > ht->nNumOfElements + (ht->nNumOfElements >> 5)) { /* additional term is there to amortize the cost of compaction */ zend_hash_rehash(ht); } else if (ht->nTableSize < HT_MAX_SIZE) { /* Let's double the table size */ void *new_data, *old_data = HT_GET_DATA_ADDR(ht); uint32_t nSize = ht->nTableSize + ht->nTableSize; Bucket *old_buckets = ht->arData; ht->nTableSize = nSize; new_data = pemalloc(HT_SIZE_EX(nSize, HT_SIZE_TO_MASK(nSize)), GC_FLAGS(ht) & IS_ARRAY_PERSISTENT); ht->nTableMask = HT_SIZE_TO_MASK(ht->nTableSize); HT_SET_DATA_ADDR(ht, new_data); memcpy(ht->arData, old_buckets, sizeof(Bucket) * ht->nNumUsed); pefree(old_data, GC_FLAGS(ht) & IS_ARRAY_PERSISTENT); zend_hash_rehash(ht); } else { zend_error_noreturn(E_ERROR, "Possible integer overflow in memory allocation (%u * %zu + %zu)", ht->nTableSize * 2, sizeof(Bucket) + sizeof(uint32_t), sizeof(Bucket)); } } // rehash(精简部分代码) ZEND_API int ZEND_FASTCALL zend_hash_rehash(HashTable *ht) { Bucket *p; uint32_t nIndex, i; if (UNEXPECTED(ht->nNumOfElements == 0)) { if (!(HT_FLAGS(ht) & HASH_FLAG_UNINITIALIZED)) { ht->nNumUsed = 0; HT_HASH_RESET(ht); } return SUCCESS; } HT_HASH_RESET(ht); i = 0; p = ht->arData; if (HT_IS_WITHOUT_HOLES(ht)) { // Bucket 中没有被标记为 IS_UNDEF 的项 do { nIndex = p->h | ht->nTableMask; Z_NEXT(p->val) = HT_HASH(ht, nIndex); HT_HASH(ht, nIndex) = HT_IDX_TO_HASH(i); p++; } while (++i < ht->nNumUsed); } else { // Bucket 中有被标记为 IS_UNDEF 的项 uint32_t old_num_used = ht->nNumUsed; do { if (UNEXPECTED(Z_TYPE(p->val) == IS_UNDEF)) { // Bucket 中第一项被标记为 IS_UNDEF uint32_t j = i; Bucket *q = p; if (EXPECTED(!HT_HAS_ITERATORS(ht))) { // hashtable 没有遍历操作 while (++i < ht->nNumUsed) { p++; if (EXPECTED(Z_TYPE_INFO(p->val) != IS_UNDEF)) { ZVAL_COPY_VALUE(&q->val, &p->val); q->h = p->h; nIndex = q->h | ht->nTableMask; q->key = p->key; Z_NEXT(q->val) = HT_HASH(ht, nIndex); HT_HASH(ht, nIndex) = HT_IDX_TO_HASH(j); if (UNEXPECTED(ht->nInternalPointer == i)) { ht->nInternalPointer = j; } q++; j++; } } } else { // hashtable 存在遍历操作 uint32_t iter_pos = zend_hash_iterators_lower_pos(ht, 0); while (++i < ht->nNumUsed) { p++; if (EXPECTED(Z_TYPE_INFO(p->val) != IS_UNDEF)) { ZVAL_COPY_VALUE(&q->val, &p->val); q->h = p->h; nIndex = q->h | ht->nTableMask; q->key = p->key; Z_NEXT(q->val) = HT_HASH(ht, nIndex); HT_HASH(ht, nIndex) = HT_IDX_TO_HASH(j); if (UNEXPECTED(ht->nInternalPointer == i)) { ht->nInternalPointer = j; } if (UNEXPECTED(i >= iter_pos)) { do { zend_hash_iterators_update(ht, iter_pos, j); iter_pos = zend_hash_iterators_lower_pos(ht, iter_pos + 1); } while (iter_pos < i); } q++; j++; } } } ht->nNumUsed = j; break; } nIndex = p->h | ht->nTableMask; Z_NEXT(p->val) = HT_HASH(ht, nIndex); HT_HASH(ht, nIndex) = HT_IDX_TO_HASH(i); p++; } while (++i < ht->nNumUsed); /* Migrate pointer to one past the end of the array to the new one past the end, so that * newly inserted elements are picked up correctly. */ if (UNEXPECTED(HT_HAS_ITERATORS(ht))) { _zend_hash_iterators_update(ht, old_num_used, ht->nNumUsed); } } return SUCCESS; }
PHP 7 中 hashtable
在扩容时也是将 nTableSize
翻倍,然后进行 rehash
。在进行 rehash
操作时,如果 Bucket
中没有标记为删除的项(IS_UNDEF
),那么 rehash
操作之后 Bucket
的存储顺序不会发生任何变化,只是 idx
索引存储的位置会因为 nTableMask
的变化而变化,最终导致 hash
碰撞链表的变化。如果 Bucket
中存在被标记为删除的项,那么在 rehash
的过程中会跳过这些 Bucket
项,只保留那些没有被删除的项。同时,由于这样会导致 Bucket
的索引相较于原来发生变化,所以在 rehash
的过程中需要同时更新 hashtable
内部指针的信息以及与遍历操作相关的信息。
⒎ PHP 7 中的 packed hashtable
在 PHP 7 中,如果一个数组为索引数组,并且数组中的索引为升序排列,那么此时由于 hashtable
中 Bucket
按照写入顺序排列,而数组索引也是升序的,所以映射表已经没有必要。PHP 7 针对这种特殊的情况对 hashtable
做了一些优化 packed hashtable
。
#define HT_MIN_MASK ((uint32_t) -2) #define HT_MIN_SIZE 8 #define HT_HASH_RESET_PACKED(ht) do { \ HT_HASH(ht, -2) = HT_INVALID_IDX; \ HT_HASH(ht, -1) = HT_INVALID_IDX; \ } while (0) static zend_always_inline void zend_hash_real_init_packed_ex(HashTable *ht) { void *data; if (UNEXPECTED(GC_FLAGS(ht) & IS_ARRAY_PERSISTENT)) { data = pemalloc(HT_SIZE_EX(ht->nTableSize, HT_MIN_MASK), 1); } else if (EXPECTED(ht->nTableSize == HT_MIN_SIZE)) { data = emalloc(HT_SIZE_EX(HT_MIN_SIZE, HT_MIN_MASK)); } else { data = emalloc(HT_SIZE_EX(ht->nTableSize, HT_MIN_MASK)); } HT_SET_DATA_ADDR(ht, data); /* Don't overwrite iterator count. */ ht->u.v.flags = HASH_FLAG_PACKED | HASH_FLAG_STATIC_KEYS; HT_HASH_RESET_PACKED(ht); }
packed hashtable
在初始化时,nTableMask
的值默认为 -2,同时在 hashtable
的 flags
中会进行相应的标记。如果此时 packed hashtable
中没有任何元素,那么 nTableSize
会设为 0。
static void ZEND_FASTCALL zend_hash_packed_grow(HashTable *ht) { HT_ASSERT_RC1(ht); if (ht->nTableSize >= HT_MAX_SIZE) { zend_error_noreturn(E_ERROR, "Possible integer overflow in memory allocation (%u * %zu + %zu)", ht->nTableSize * 2, sizeof(Bucket), sizeof(Bucket)); } ht->nTableSize += ht->nTableSize; HT_SET_DATA_ADDR(ht, perealloc2(HT_GET_DATA_ADDR(ht), HT_SIZE_EX(ht->nTableSize, HT_MIN_MASK), HT_USED_SIZE(ht), GC_FLAGS(ht) & IS_ARRAY_PERSISTENT)); }
另外,packed hashtable
在扩容时,只需要将 nTableSize
翻倍,同时由于索引是升序排列的,所以 Bucket
的顺序不需要做任何调整,只需要重新分配内存空间即可。
需要强调的是,
packed hashtable
只适用于索引为升序排列的索引数组(索引不一定要连续,中间可以有间隔)。如果索引数组的索引顺序被破坏,或索引中加入了字符串索引,那么此时packed hashtable
会被转换为普通的hashtable
。
推荐学习:《PHP视频教程》
以上是數組實作方式:PHP5 VS PHP7的詳細內容。更多資訊請關注PHP中文網其他相關文章!

PHP仍然流行的原因是其易用性、靈活性和強大的生態系統。 1)易用性和簡單語法使其成為初學者的首選。 2)與web開發緊密結合,處理HTTP請求和數據庫交互出色。 3)龐大的生態系統提供了豐富的工具和庫。 4)活躍的社區和開源性質使其適應新需求和技術趨勢。

PHP和Python都是高層次的編程語言,廣泛應用於Web開發、數據處理和自動化任務。 1.PHP常用於構建動態網站和內容管理系統,而Python常用於構建Web框架和數據科學。 2.PHP使用echo輸出內容,Python使用print。 3.兩者都支持面向對象編程,但語法和關鍵字不同。 4.PHP支持弱類型轉換,Python則更嚴格。 5.PHP性能優化包括使用OPcache和異步編程,Python則使用cProfile和異步編程。

PHP主要是過程式編程,但也支持面向對象編程(OOP);Python支持多種範式,包括OOP、函數式和過程式編程。 PHP適合web開發,Python適用於多種應用,如數據分析和機器學習。

PHP起源於1994年,由RasmusLerdorf開發,最初用於跟踪網站訪問者,逐漸演變為服務器端腳本語言,廣泛應用於網頁開發。 Python由GuidovanRossum於1980年代末開發,1991年首次發布,強調代碼可讀性和簡潔性,適用於科學計算、數據分析等領域。

PHP適合網頁開發和快速原型開發,Python適用於數據科學和機器學習。 1.PHP用於動態網頁開發,語法簡單,適合快速開發。 2.Python語法簡潔,適用於多領域,庫生態系統強大。

PHP在現代化進程中仍然重要,因為它支持大量網站和應用,並通過框架適應開發需求。 1.PHP7提升了性能並引入了新功能。 2.現代框架如Laravel、Symfony和CodeIgniter簡化開發,提高代碼質量。 3.性能優化和最佳實踐進一步提升應用效率。

PHPhassignificantlyimpactedwebdevelopmentandextendsbeyondit.1)ItpowersmajorplatformslikeWordPressandexcelsindatabaseinteractions.2)PHP'sadaptabilityallowsittoscaleforlargeapplicationsusingframeworkslikeLaravel.3)Beyondweb,PHPisusedincommand-linescrip

PHP類型提示提升代碼質量和可讀性。 1)標量類型提示:自PHP7.0起,允許在函數參數中指定基本數據類型,如int、float等。 2)返回類型提示:確保函數返回值類型的一致性。 3)聯合類型提示:自PHP8.0起,允許在函數參數或返回值中指定多個類型。 4)可空類型提示:允許包含null值,處理可能返回空值的函數。


熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

AI Hentai Generator
免費產生 AI 無盡。

熱門文章

熱工具

PhpStorm Mac 版本
最新(2018.2.1 )專業的PHP整合開發工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

SAP NetWeaver Server Adapter for Eclipse
將Eclipse與SAP NetWeaver應用伺服器整合。

EditPlus 中文破解版
體積小,語法高亮,不支援程式碼提示功能

DVWA
Damn Vulnerable Web App (DVWA) 是一個PHP/MySQL的Web應用程序,非常容易受到攻擊。它的主要目標是成為安全專業人員在合法環境中測試自己的技能和工具的輔助工具,幫助Web開發人員更好地理解保護網路應用程式的過程,並幫助教師/學生在課堂環境中教授/學習Web應用程式安全性。 DVWA的目標是透過簡單直接的介面練習一些最常見的Web漏洞,難度各不相同。請注意,該軟體中