PHP7原始碼中的優雅設計
團隊內分享PHP7原始碼,重讀程式碼過程中發現其中不少優秀設計之處,整理一篇其原始碼中的優雅設計。
閱讀要求:對PHP7原始碼實現有一定了解,具備一定的原始碼分析能力
推薦幾篇優秀的文章,建議先行閱讀:
- Array/HashTable實現,推薦閱讀 ofollow,noindex">Julien Pauli-PHP 7 Arrays : HashTables
- 鳥哥Laruence的slide: The secret of PHP7’s Performance
Array如何保證有序
問題:在PHP中Array陣列是通過HashTable雜湊來實現,但由於Hash的特性是高效訪問、但資料無序,因此面臨陣列遍歷時順序的問題?
先來看看陣列Array的實現
陣列的兩個重要結構體:
-
Bucket
:單個元素的儲存單元 -
_zend_array
別名HashTable
:陣列的上層封裝typedef struct _Bucket { zvalval; zend_ulongh; /* 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_ucharflags, zend_ucharnApplyCount, zend_ucharnIteratorsCount, zend_ucharreserve) } v; uint32_t flags; } u; uint32_tnTableMask; Bucket*arData; uint32_tnNumUsed; uint32_tnNumOfElements; uint32_tnTableSize; uint32_tnInternalPointer; zend_longnNextFreeElement; dtor_func_tpDestructor; };
老生常談 _zend_array
:
-
gc
:引用計數 -
u
:聯合體flags
或v
標誌位 -
nTableMask
:掩碼, = -nTableSize -
*arData
:指向資料元素儲存Bucket地址 -
nNumUsed
:陣列內已使用空間數量(unset元素後nNumUsed不變,nNumOfElements減少) -
nNumOfElements
:陣列內有效元素個數 -
nTableSize
:陣列空間開闢大小 -
nInternalPointer
:待補充 -
nNextFreeElement
:下一個可用元素位置 -
pDestructor
:析構時處理
HashTable巧妙之處: nTableMask
-
nTableMask = -nTableSize
:為什麼同樣一個nTableSize
數值,額外用nTableMask
冗餘一份呢?- 通過位運算計算nIndex
nIndex = p->h | ht->nTableMask
-
h
是key
進行hash計算後的雜湊值,與nTableMask
(補碼錶示,nTableSize
反碼+1)或運算,取值範圍[0, nTableSize-1]
- 實現效果與
nIndex = p->h % ht->nTableSize
相同,但 位或運算效率比模運算高 很多 - 空間 VS 時間 效率的博弈,這裡冗餘一個欄位,打打提升頻繁
nIndex
計算的效率
- 通過位運算計算nIndex
HashTable巧妙之處: nNumUsed
和 nNumOfElemets
-
nNumUsed
和nNumOfElemets
為何區分開?- 釋放中間元素時不做記憶體處理,保證高效,僅標記元素
p->val->u1.v.type=IS_UNDEF
- 在
resize()
或rehash()
時將已刪除的IS_UNDEF
元素進行記憶體重整
- 釋放中間元素時不做記憶體處理,保證高效,僅標記元素
Array巧妙之處: arData
、 nIndex
、 idx
- Array底層使用HashTable儲存,如何保證插入陣列元素的有序性?
- 先重點看下arData指向的Bucket內部結構如下:
- 上圖例子資料寫入過程:
-
nTableSize=8
,nTableMask = -nTableSize = -8
- 陣列首次寫入元素
$array['bar] = 'bar-val'
時,h
為bar
經過Time33
演算法計算後的數值,nIndex = h | nTableMask = -3
-
idx=nNumUsed++
、arData[nIdex] = idx
,從而寫入對映表arData[-3] = 0
,資料寫入arData[idx]=Bucket{key,h,val}
也就是arData[0]={'bar',hash(bar),'bar-var'}
- 相同的,插入
$array['foo'] = 42
時,寫入對映表arData[-5]=1
,資料寫入arData[idx]=Bucket{key,h,val}
也就是arData[1]={'foo',hash(foo),42}
-
-
-
arData
指向區域包含兩部分:hash對映表
和資料儲存Buckets
,後者Buckets為資料儲存區。如直接hash取模的方式儲存(雜湊值跳躍且分散),則遍歷時無法保證順序,因此衍生出通過hash對映表
來實現的方式 -
arData
指向Buckets儲存區的起始位置,而hash對映表
在其負值索引位置上,nIndex為負值,通過陣列的負值索引快速訪問arData[nIndex]
值 - 具體來說,根據
nNumUsed
確定首個可用Buckets索引地址idx,繼而計算nIndex(nIndex = h | nTableMask
),將資料在Buckets區的儲存索引idx儲存到對映表:arData[nIndex] = idx
- 索引查詢時,按照
h->nIndex->idx
的順序查詢資料,幾乎是O(1)複雜度的 - 順序遍歷時,按照Buckets區逐一遍歷即使插入時順序
- 巧妙的 ,這裡將對映表和資料區連續記憶體空間儲存,且nIndex通過
h|nTableMask
的方式快速計算獲得,極大保證計算效率;連續分配,釋放、擴容時都是簡單高效的處理方式
- 先重點看下arData指向的Bucket內部結構如下:
最終陣列的儲存結構:(圖片來源鳥哥分享slide)

zend_string中變長陣列
zend_string
結構體定義:
struct _zend_string { zend_refcounted_h gc; zend_ulongh; /* hash value */ size_tlen; charval[1]; };
gc h len val[1]
zend_string
巧妙之處: val[1]
- 變長陣列( Variable-length array )是在 C99" target="_blank" rel="nofollow,noindex">ISO C99 之後才支援的特性,使用此特性需要編譯器支援C99標準。
- 零長陣列 是GNU C版本編譯器支援,並引導C99最終支援變長陣列的經典案例,但不同版本實現的編譯器可能 不支援零長陣列 。
- 在PHP7原始碼中為了相容不同版本編譯器、利用變長陣列特性,使用
val[1]
來實現固定頭部的可變物件
的儲存形式。
後續補充:
巧妙之處:IS_UNDEF
TODO:刪除時設定為 IS_UNDEF
,在需要時統一進行記憶體整理提高單次操作效能。