【PHP7源碼學習】剖析PHP數組的有序性

baiyanphp

引入案例

在 PHP7中,咱們往數組中插入元素的順序,就決定了咱們數組遍歷元素的順序。能夠說,PHP7中的數組是有序的。這個有序就是指元素插入數組時的順序,與遍歷時順序的一致性。爲了直觀地讓你們瞭解到PHP7數組的有序性,請看下面一段PHP代碼:數組

<?php
$a = [];
$a['insert1'] = 'baiyan1';
$a['insert2'] = 'baiyan2';
$a['insert3'] = 'baiyan3';
foreach ($a as $k => $v) {
    var_dump($k . ':' . $v);
}

咱們按照一、二、3的順序向數組中插入key-value對,而後在循環體中打印遍歷的順序,結果以下:ui

string(15) "insert1:baiyan1"
string(15) "insert2:baiyan2"
string(15) "insert3:baiyan3"

而後咱們反轉插入元素的順序,以三、二、1的順序插入,其他代碼不變:spa

<?php
$a = [];
$a['insert3'] = 'baiyan3';
$a['insert2'] = 'baiyan2';
$a['insert1'] = 'baiyan1';
foreach ($a as $k => $v) {
    var_dump($k . ':' . $v);
}

一樣的,打印結果以下:3d

string(15) "insert3:baiyan3"
string(15) "insert2:baiyan2"
string(15) "insert1:baiyan1"

觀察以上兩組輸出結果,咱們能夠看到,往數組中插入元素的順序決定了遍歷的順序,PHP數組是有序的。指針

普通哈希表的問題:無序性

哈希表的無序性是指元素插入順序遍歷順序不一致性。在PHP7中,爲了達到查找某個key的複雜度爲O(1),其內部是以hashtable的結構來實現的。先拋開PHP的實現不說,首先咱們舉一個通常的例子。一般狀況下,一個hashtable長這樣,每一個存儲單元被稱爲一個bucket(桶):

這個哈希表很普通,它的大小爲8,目前尚未任何元素插入,接下來咱們插入上面的三條數據,假設對其key進行哈希運算的結果分別爲四、二、6,插入以後的情形以下(key和value原本應該綁定在一塊兒的,爲了簡化故省略value的書寫):

咱們想一下,這樣存儲的問題都有哪些:code

  • 元素之間的分佈很零散,在擴容或縮容的時候很差處理
  • 插入與遍歷的無序性

第一條不是咱們此篇文章的重點。咱們在遍歷這個數組的時候,單看這張圖,咱們是不知道插入的順序是什麼樣的,只能經過insert二、insert一、insert3的順序遍歷。因此,遍歷的順序與插入的insert一、insert二、insert3的順序並不吻合,並不能達到咱們在PHP7中數組的預期。blog

PHP7數組:解決普通哈希表的無序性問題

爲了實現插入與遍歷的順序一致性,在PHP7中,增長了一箇中間映射層,它的大小與哈希表相同,存儲了元素在bucket中最終存儲的位置,咱們把它叫作映射表。這樣說可能你們還不太明白,讓咱們用圖解一步一步來複現上一個案例的插入過程。咱們先忽略哈希衝突的問題。首先咱們插入insert1這個key-value對:

首先,假設對key insert1的哈希運算結果爲4,因爲如今哈希表中的全部bucket均爲空,因此咱們能夠利用第一個bucket空間來存儲這個insert1。爲了讓後續的查找等操做可以順利找到insert1,咱們在映射表中下標爲4的地方記錄下insert1存儲的位置,即bucket的下標0。這樣,在查找的時候,根據這個hash值4,經過映射表就可以順利找到insert1在bucket中存儲的位置0。
而後咱們繼續插入insert2這對key-value對,同理,咱們直接日後找可用的bucket,下標爲1的bucket就是可用的,那麼咱們準備把insert2存入這裏,同時利用映射表記下存儲的bucket下標1:

假設對key insert2的哈希運算結果爲2,因爲下一個可用的bucket下標爲1,咱們須要記錄下這個1,而它的哈希運算結果爲2,咱們就在映射表下標爲2的位置記錄下insert2的存儲bucket位置1。
到這裏,咱們能夠發現,咱們插入新元素的時候,會直接日後尋找可用的bucket位置,而這個位置是和以前插入的元素牢牢相鄰的。這樣,咱們在foreach循環的時候,直接對這個bucket進行遍歷,其遍歷結果就是有序的。
若是你尚未明白,咱們繼續往中插入insert3這對key-value對:

假設對key insert3的哈希運算結果爲6,咱們直接日後尋找可用的bucket,下標爲2。咱們須要記錄下這個2,因而在映射表下標爲哈希值運算結果6的位置,存儲下這個下標2便可。
這樣一來,咱們直接去遍歷這個hashtable,從bucket下標爲0開始直接遍歷到末尾,就可以獲得與插入時候一摸同樣的順序,即insert一、insert二、insert3了,且元素之間沒有碎片,提升了hashtable的空間利用率,方便擴容與縮容。
到這裏,咱們應該清楚了這個映射表的做用實現PHP7數組的插入與遍歷順序一致性
在PHP7中,爲了方便映射表的訪問,沒有將映射表的空間額外單獨地分配,而是直接分配在與hashtable中緊挨着的前一塊相鄰的內存空間中,這樣經過一個指針,就能夠同時訪問映射表每個bucket啦:

在PHP7中,因爲映射表的下標爲負值,爲了實現相同的功能,不能用咱們以前直接使用哈希值作下標來存儲bucket的位置,而是須要通過一步計算:索引

nIndex = h | nTableMask

由此,咱們最後來看一下PHP中hashtable的結構,最重要的就是這個arData指針。若是在上圖中表示,就是中間那個豎直的分界線啦。經過以正索引和負索引訪問數組的方式,咱們就能夠同時訪問映射表和哈希表中的bucket:內存

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;  //映射表以及哈希表的指針,利用arData[-x]訪問映射表,利用arData[+x]訪問哈希表中的bucket
    uint32_t          nNumUsed;
    uint32_t          nNumOfElements;
    uint32_t          nTableSize;
    uint32_t          nInternalPointer;
    zend_long         nNextFreeElement;
    dtor_func_t       pDestructor;
};

typedef struct _zend_array HashTable;

因爲咱們這篇文章沒有提到哈希衝突的問題,咱們這裏講到的是最簡單的插入狀況。至於在 PHP中如何解決插入時產生的哈希衝突問題,其實是使用了數組模擬鏈表的思想,這裏再也不展開,之後我會再開一篇專題來進行講解。

相關文章
相關標籤/搜索