聊聊PHP數組底層是如何實現的

最近一直整理資料,搞的自己挺煩躁的,靜下心,寫一篇博客壓壓驚。
今天就聊聊PHP最核心的數組array;
話不多說,直接打開源碼,看看array到底是如何實現的?
PHP 數組的底層實現是散列表(也叫 hashTable ),散列表是根據鍵(Key)直接訪問內存存儲位置的數據結構,
它的key - value 之間存在一個映射函數,可以根據 key 通過映射函數得到的散列值直接索引到對應的 value 值,
無需通過關鍵字比較,在理想情況下,不考慮散列衝突,散列表的查找效率是非常高的,時間複雜度是 O(1)
直接翻閱到了zend引擎定義數據類型的地方就是開始了:

typedef struct _zval_struct     zval;
typedef struct _zend_refcounted zend_refcounted;
typedef struct _zend_string     zend_string;
typedef struct _zend_array      zend_array; //數組
typedef struct _zend_object     zend_object;
typedef struct _zend_resource   zend_resource;

數組在C的底層就是一個結構體了,接下來尋找結構體的實現:
又繼續閱讀找到了_zend_array的定義:

struct _zend_array {
	zend_refcounted_h gc;
	union {
		struct {
			ZEND_ENDIAN_LOHI_4(
				zend_uchar    flags,
				zend_uchar    nApplyCount,
				zend_uchar    nIteratorsCount,
				zend_uchar    consistency)
		} v;
		uint32_t flags;
	} u;
	uint32_t          nTableMask;
	Bucket           *arData; //存儲元素數組,指向第一個Bucket
	uint32_t          nNumUsed;//哈希表已經使用的元素數
	uint32_t          nNumOfElements; // 哈希表有效元素數
	uint32_t          nTableSize; // 哈希表總大小,爲2的n次方(包含無效的元素)
	uint32_t          nInternalPointer; // 內部指針,用於遍歷
	zend_long         nNextFreeElement; // 下一個可用的數值索引,如:arr[] = 1;
	dtor_func_t       pDestructor;
};

//定義hashtable別名
typedef struct _zend_array HashTable;
//一些php數組的函數也是hashtable聲明類型的
PHPAPI int php_array_merge(HashTable *dest, HashTable *src);
PHPAPI int php_array_merge_recursive(HashTable *dest, HashTable *src);
PHPAPI int php_array_replace_recursive(HashTable *dest, HashTable *src);

Bucket的實現:

typedef struct _Bucket {
	zval              val;              // 存儲的具體 value
	zend_ulong        h;                /* key 的哈希值。用於查找時 key 的比較   */
	zend_string      *key;              /* 當 key 值爲字符串時,指向該字符串對應的 zend_string(使用數字索引時該值爲 NULL) */
} Bucket;

zval的實現:

struct _zval_struct {
	zend_value        value;			/* value */
	union {
		struct {
			ZEND_ENDIAN_LOHI_4(
				zend_uchar    type,			/* active type */
				zend_uchar    type_flags,
				zend_uchar    const_flags,
				zend_uchar    reserved)	    /* call info for EX(This) */
		} v;
		uint32_t type_info;
	} u1;
	union {
		uint32_t     next;                 /* hash collision chain */
		uint32_t     cache_slot;           /* literal cache slot */
		uint32_t     lineno;               /* line number (for ast nodes) */
		uint32_t     num_args;             /* arguments number for EX(This) */
		uint32_t     fe_pos;               /* foreach position */
		uint32_t     fe_iter_idx;          /* foreach iterator index */
		uint32_t     access_flags;         /* class constant access flags */
		uint32_t     property_guard;       /* single property guard */
		uint32_t     extra;                /* not further specified */
	} u2;
};

存儲在散列表裏的元素是無序的,PHP 數組如何做到按順序讀取的呢?
中間映射表,爲了實現散列表的有序性,PHP 爲其增加了一張中間映射表,該表是一個大小與 Bucket 相同的數組,數組中儲存整形數據,用於保存元素實際儲存的 Value 在 Bucekt 中的下標。Bucekt 中的數據是有序的,而中間映射表中的數據是無序的。
而通過映射函數映射後的散列值要在中間映射表的區間內,這就對映射函數提出了要求。
將 key 經過 time33 算法生成的哈希值 h 和 nTableMask 進行或運算即可得出映射表的下標,其中 nTableMask 數值爲 nTableSize 的負數。並且由於 nTableSize 的值爲 2 的冪次方,所以 nTableMask 二進制位右側全部爲 0,保證了 h | ht->nTableMask 的取值範圍會在 [-nTableSize, -1] 之間,正好在映射表的下標範圍內。另外,用按位或運算的方法和其他方法如取餘的方法相比運算速度較高,這個映射函數可以說設計的非常巧妙了。

解決hash衝突:
在衝突位置構造一個單向鏈表,將散列值相同的元素放到相同槽位對應的鏈表中。這個方法叫鏈地址法,PHP 數組就是採用這個方法解決散列衝突的問題。
其具體實現是:將衝突的 Bucket 串成鏈表,這樣中間映射表映射出的就不是某一個元素,而是一個 Bucket 鏈表,通過散列函數定位到對應的 Bucket 鏈表時,需要遍歷鏈表,逐個對比 Key 值,繼而找到目標元素。而每個 Bucket 之間的鏈接則是將原 value 的下標保存到新 value 的 zval.u2.next 裏,新 value 放在當前位置上,從而形成一個單向鏈表。

數組擴容:
PHP 的數組在底層實現了自動擴容機制,當插入一個元素且沒有空閒空間時,就會觸發自動擴容機制,擴容後再執行插入。

擴容的過程爲:

如果已刪除元素所佔比例達到閾值,則會移除已被邏輯刪除的 Bucket,然後將後面的 Bucket 向前補上空缺的 Bucket,因爲 Bucket 的下標發生了變動,所以還需要更改每個元素在中間映射表中儲存的實際下標值。

如果未達到閾值,PHP 則會申請一個大小是原數組兩倍的新數組,並將舊數組中的數據複製到新數組中,因爲數組長度發生了改變,所以 key-value 的映射關係需要重新計算,這個步驟爲重建索引。

rehash:

在刪除某一個數組元素時,會先使用標誌位對該元素進行邏輯刪除,即在刪除 value 時只是將 value 的 type 設置爲 IS_UNDEF,而不會立即刪除該元素所在的 Bucket,因爲如果每次刪除元素立刻刪除 Bucket 的話,每次都需要進行排列操作,會造成不必要的性能開銷。

所以,當刪除元素達到一定數量或擴容後都需要重建散列表,即移除被標記爲刪除的 value。因爲 value 在 Bucket 位置移動了或哈希數組 nTableSize 變化了導致 key 與 value 的映射關係改變,重建過程就是遍歷 Bucket 數組中的 value,然後重新計算映射值更新到散列表。

hash算法應用很多,比如redis的hashtable實現原理也是這樣的。

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