PHP7變量的內部實現(一)

PHP7變量的內部實現-part 1

本文翻譯自Nikita的文章,水平有限,若有錯誤,歡迎指正查看原文php

受篇幅限制,這篇文章將分爲兩個部分。本部分會講解PHP5和PHP7在zval結構體的差別,同時也會討論引用的實現。第二部分會深刻探究一些數據類型如string和對象的實現。html

PHP5中的zval

PHP5中zval結構體的定義以下:node

typedef struct _zval_struct {
    zvalue_value value;
    zend_uint refcount__gc;
    zend_uchar type;
    zend_uchar is_ref__gc;
} zval;

能夠看到,zval由value、type和一些額外的__gc信息組成。__gc與垃圾回收相關,咱們稍後討論。value是一個共用體,能夠存儲y一個zval各類可能的值。git

typedef union _zvalue_value {
    long lval;                 // For booleans, integers and resources
    double dval;               // For floating point numbers
    struct {                   // For strings
        char *val;
        int len;
    } str;
    HashTable *ht;             // For arrays
    zend_object_value obj;     // For objects
    zend_ast *ast;             // For constant expressions
} zvalue_value;

C語言中,共用體的尺寸與它最大的成員尺寸相同,在某一時刻只能有一個成員處於活動狀態。共用體全部的成員都存儲在相同的內存,根據你訪問的成員不一樣,內容會被解釋成不一樣的類型。以上面的共用體爲例,若是訪問lval,值將被解釋爲一個有符號整型;而訪問dval將被解釋成雙精度浮點型。以此類推。github

爲了弄清結構體中哪一個成員處於活動狀態,zval會存儲一個整型type來標識具體的數據類型。express

#define IS_NULL     0      /* Doesn't use value */
#define IS_LONG     1      /* Uses lval */
#define IS_DOUBLE   2      /* Uses dval */
#define IS_BOOL     3      /* Uses lval with values 0 and 1 */
#define IS_ARRAY    4      /* Uses ht */
#define IS_OBJECT   5      /* Uses obj */
#define IS_STRING   6      /* Uses str */
#define IS_RESOURCE 7      /* Uses lval, which is the resource ID */

/* Special types used for late-binding of constants */
#define IS_CONSTANT 8
#define IS_CONSTANT_AST 9

PHP5中的引用計數

除少數例外,在PHP5中zval都是分配在堆內存的,PHP須要經過某種方式跟蹤哪些zval在被使用,哪些應該被釋放。爲達到這個目的,引用計數被使用。引用計數即在結構體中用refcount__gc成員來記錄該結構體被「引用」了多少次。例如,在$a = $b = 42中,42被兩個變量引用,因此它的引用計數爲2。若是引用計數變成0,則意味着該值沒被使用,能夠被釋放。windows

須要注意的是引用計數的「引用」(即一個值被引用的次數)與「PHP引用」($a=&$b)毫無關係。在接下來的內容裏,我會始終使用「引用」和「PHP引用」這兩個術語來釋疑這兩個概念。就當前來講,咱們先把「PHP引用」放在一邊。數組

與引用計數密切相關的一個概念是「寫時複製」(copy on write):zval只能在其內容未被修改的時候才能在多個變量間共享。要實現修改,zval必選被複制(分離),而改動只能在複製出的zval上進行。安全

如下例子展現了寫時複製和zval銷燬。php7

$a = 42;   // $a         -> zval_1(type=IS_LONG, value=42, refcount=1)
$b = $a;   // $a, $b     -> zval_1(type=IS_LONG, value=42, refcount=2)
$c = $b;   // $a, $b, $c -> zval_1(type=IS_LONG, value=42, refcount=3)

// 下一行操做會致使zval分離
$a += 1;   // $b, $c -> zval_1(type=IS_LONG, value=42, refcount=2)
           // $a     -> zval_2(type=IS_LONG, value=43, refcount=1)

unset($b); // $c -> zval_1(type=IS_LONG, value=42, refcount=1)
           // $a -> zval_2(type=IS_LONG, value=43, refcount=1)

unset($c); // zval_1 被銷燬,由於其refcount=0
           // $a -> zval_2(type=IS_LONG, value=43, refcount=1)

引用計數有一個致命缺陷:它不能檢測和釋放循環引用。爲解決這個問題,PHP額外使用了環收集器。當一個zval的引用計數減小的時候,它就有必定概率是循環引用的一部分,該zval就被寫入到「根緩衝區」。當根緩衝區滿後,可能的引用環將被標記並收集,同時啓動垃圾回收。

爲了支持這個環收集器,實際使用了以下的zval結構體:

typedef struct _zval_gc_info {
    zval z;
    union {
        gc_root_buffer       *buffered;
        struct _zval_gc_info *next;
    } u;
} zval_gc_info;

zval_gc_info結構體內置了普通zval和一個指針-注意u是一個共用體,也就是說實際上只有一個指針,它可能指向兩種不一樣的類型。buffered指針用來存儲zval在根緩衝區中的引用位置,若是zval在環收集器運行以前就被銷燬(這是很是可能的),那麼該指針將會從根緩衝區移除。next指針在收集器銷燬值的時候會被用到,可是我不會深刻講解這一點。

改進的動機

先討論一下基於64位系統的內存佔用。首先,zvalue_value共用體佔用16個字節,由於它的str和obj成員都那麼大。整個zval結構體一共24個字節(因爲內存對齊[padding]),而zval_gc_info是32字節。除此以外,在堆分配的過程當中,又增長了16字節的分配開銷。由此一個zval就佔用48字節--儘管該zval可能在多個地方都被用到。

如今咱們就能夠分析下這種zval實現方式低效的地方。考慮用zval存儲整數的狀況,整數佔用8個字節,另外類型標示是必需的,它自己佔用一個字節,可是因爲內存對齊,實際上就要加上8個字節。

這16字節是咱們真正「須要」的空間(近似的),此外,爲了處理引用計數和垃圾回收,咱們增長了16字節;因爲分配開銷又增長了另外16字節。更不用提還要處理分配和後續的釋放,這都是很昂貴的操做。

由此引起了一個問題:一個簡單的整數真的須要存儲爲一個有引用計數、可垃圾回收,而且是堆分配的值嗎?答案固然是不須要,這樣作是沒道理的。

如下概述了PHP5中zval實現方式的一些主要問題:

  • zval(幾乎)老是須要堆分配。
  • zval老是會被引用計數且攜帶環收集信息,即便是在共享值不划算(好比整數)和不能造成引用環的狀況下。
  • 當處理對象和資源時,直接對zval進行引用計數會致使雙重計數。緣由會在下一部分討論。
  • 某些狀況會引入不少的間接操做。好比爲了訪問一個對象,一共要進行4次指針跳轉。這也將在下一篇中分析。
  • 直接對zval進行引用計數意味着值只能在zval間共享。好比咱們不能在zval和哈希表key之間共享一個字符串(不將哈希表key用zval變量存放)。

PHP7中的zval

經過以上討論,咱們引進了PHP7新的zval實現。最根本的改變是zval再也不是堆分配且它自身再也不存儲引用計數。相反的,對zval指向的任何複雜類型值(如字符串、數組、對象),這些值將本身存儲引用計數。這有如下優勢:

  • 簡單值不須要分配且不用引用計數。
  • 再也不有雙重引用計數。對對象來講,只有在對象自己存在引用計數。
  • 因爲引用計數保存在值中,這個能夠獨立於zval結構而被複用。同一個字符串能同時被zval和哈希表key引用。
  • 間接操做少了不少,也就是說在獲取一個值的時候須要跳轉的指針數量變少了。

新的zval定義以下:

struct _zval_struct {
    zend_value value;
    union {
        struct {
            ZEND_ENDIAN_LOHI_4(
                zend_uchar type,
                zend_uchar type_flags,
                zend_uchar const_flags,
                zend_uchar reserved)
        } v;
        uint32_t type_info;
    } u1;
    union {
        uint32_t var_flags;
        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
    } u2;
};

第一個成員跟以前相似,也是一個value共同體。第二個成員是個整數,用來存儲類型信息,它被一個共用體分隔成獨立的字節空間(可忽略ZEND_ENDIAN_LOHI_4宏,它是用來保證在不一樣字節序平臺上佈局的一致性)。這個子結構中type(它跟以前相似)和type_flags比較重要,我將稍後討論他們。

此時有一個小問題:value成員佔8字節空間,因爲結構體內存對齊,即便增長一個字節也會讓zval內存增加到16字節。然而很明顯咱們不須要8個字節來僅僅存放類型信息。這就是爲何此zval包含了一個額外的u2共用體,它默認狀況下是沒被佔用的,可是卻能夠根據須要存儲4字節的數據。這個共用體中不一樣的成員用來實現該額外數據片斷不一樣的用途。

PHP7中的value共用體看起來略有不一樣:

typedef union _zend_value {
    zend_long         lval;
    double            dval;
    zend_refcounted  *counted;
    zend_string      *str;
    zend_array       *arr;
    zend_object      *obj;
    zend_resource    *res;
    zend_reference   *ref;
    zend_ast_ref     *ast;

    // Ignore these for now, they are special
    zval             *zv;
    void             *ptr;
    zend_class_entry *ce;
    zend_function    *func;
    struct {
        ZEND_ENDIAN_LOHI(
            uint32_t w1,
            uint32_t w2)
    } ww;
} zend_value;

首先要注意到這個共用體佔用8字節而不是16字節。它僅僅會直接存儲整數(lval)和雙精度浮點數(dval),對其它類型它都會存儲對應指針。全部的指針類型(除了什麼代碼中標記爲特殊的)都會引用計數而且有一個通用的頭部,定義爲zend_refcounted:

struct _zend_refcounted {
    uint32_t refcount;
    union {
        struct {
            ZEND_ENDIAN_LOHI_3(
                zend_uchar    type,
                zend_uchar    flags,
                uint16_t      gc_info)
        } v;
        uint32_t type_info;
    } u;
};

不用說這個結構會包含引用計數。另外,它還包含type、flags和gc_info。type是複製的zval的type,它使得GC在不存儲zval的狀況下就能區分不一樣的引用計數結構。根據類型的不一樣,flags有不一樣的使用目的,這些會在下一部分按類型分別討論。

gc_info等同於老zval中的buffered成員。不一樣的是它存儲了在根緩衝區中的索引,來代替以前的指針。由於跟緩衝區尺寸固定(10000個元素),用16字節的數子而不是64位的指針就足夠了。gc_info還含有該節點的「顏色」信息,這在垃圾回收中用來標記節點。

zval內存管理

我已經提到zval再也不是單獨的堆分配。然而很明顯它仍然須要被存在某個地方,那麼這是怎麼實現的呢?儘管zval大多數時候還是堆分配數據結構的一部分,不過它們是直接嵌入到這些數據結構中的。好比哈希表就會直接內置zval而不是存放一個指向另外一zval的指針。函數的編譯變量表或者對象的屬性表會直接保存爲一個擁有連續內存的zval數組,而再也不存儲指向散落各處zval的指針。所以當前的zval存儲一般都會少了一層的間接引用,也就是說如今的zval至關於以前的zval*。

當一個zval在新的地方被引用時,按照以前的方式,就意味着要複製zavl*並增長它的引用計數。如今則須要複製zval的內容,同時若是該zval指向的值用到引用計數的話則還要增長該值的引用計數。

PHP是如何知道一個值是否用到引用計數的呢?這不能僅僅依靠類型來判斷,由於有些類型好比字符串和數組並不老是引用計數的。相反的,會根據構成zval的type_info的一個字節來判斷是否引用計數。另外還有其它幾個字節編碼了該類型的一些特徵。

#define IS_TYPE_CONSTANT            (1<<0)   /* special */
#define IS_TYPE_IMMUTABLE           (1<<1)   /* special */
#define IS_TYPE_REFCOUNTED          (1<<2)
#define IS_TYPE_COLLECTABLE         (1<<3)
#define IS_TYPE_COPYABLE            (1<<4)
#define IS_TYPE_SYMBOLTABLE         (1<<5)   /* special */

一個類型能擁有的三個主要特徵是引用計數、可回收和可複製。引用計數的含義已討論過,可回收意味着該zval可能參與循環引用。舉例來講,字符串(一般)是引用計數的,可是卻無法用字符串構造一個引用環。

可複製性決定了在爲一個變量建立「副本」的時候它的值是否須要執行拷貝。副本是硬拷貝,好比複製指向數組的zval時,就不是簡單的增長數組的引用計數,而是要建立該數組的一個新的獨立拷貝。然而對對象和資源這些類型來講,複製應該僅僅增長引用計數--這些類型就是所謂的不可複製。這與對象和資源在進行傳遞時的語義相符(當前不是引用傳遞)。

如下表格展現了不一樣類型和它們所用的標識。「簡單類型」指整數和布爾值這類不須要用指針指向一個單獨結構的類型。同時還用一列展現了「不可變」標記,它用來標記不可變數組,這將在下一部分詳細討論。

| refcounted | collectable | copyable | immutable
----------------+------------+-------------+----------+----------
simple types    |            |             |          |
string          |      x     |             |     x    |
interned string |            |             |          |
array           |      x     |      x      |     x    |
immutable array |            |             |          |     x
object          |      x     |      x      |          |
resource        |      x     |             |          |
reference       |      x     |             |          |

來看一下在實際中zval管理是如何工做的。先基於上文PHP5的例子來討論一下整型實現:

$a = 42;   // $a = zval_1(type=IS_LONG, value=42)

$b = $a;   // $a = zval_1(type=IS_LONG, value=42)
           // $b = zval_2(type=IS_LONG, value=42)

$a += 1;   // $a = zval_1(type=IS_LONG, value=43)
           // $b = zval_2(type=IS_LONG, value=42)

unset($a); // $a = zval_1(type=IS_UNDEF)
           // $b = zval_2(type=IS_LONG, value=42)

這個例子挺無趣的。簡單來講就是整型不會再被共用,這些變量都有單獨的zval。不要忘了zval再也不須要單獨分配,它們是內嵌的,我經過把->換成=來表示這種變化。unset一個變量會把對應zval的type設置爲IS_UNDEF。如今來考慮一下當涉及複雜類型時的狀況,這種案例有趣的多。

$a = [];   // $a = zval_1(type=IS_ARRAY) -> zend_array_1(refcount=1, value=[])

$b = $a;   // $a = zval_1(type=IS_ARRAY) -> zend_array_1(refcount=2, value=[])
           // $b = zval_2(type=IS_ARRAY) ---^

// zval在這裏發生了分離
$a[] = 1   // $a = zval_1(type=IS_ARRAY) -> zend_array_2(refcount=1, value=[1])
           // $b = zval_2(type=IS_ARRAY) -> zend_array_1(refcount=1, value=[])

unset($a); // $a = zval_1(type=IS_UNDEF) and zend_array_2 is destroyed
           // $b = zval_2(type=IS_ARRAY) -> zend_array_1(refcount=1, value=[])

本例中每一個變量依然有單獨的zval(內嵌的),可是這些zval都指向了同一個zend_array(引用計數的)結構。同PHP5同樣,當發生修改時,數組須要被複制。

類型

看一下PHP7是如何支持各類數據類型的:

// regular data types
#define IS_UNDEF                    0
#define IS_NULL                     1
#define IS_FALSE                    2
#define IS_TRUE                     3
#define IS_LONG                     4
#define IS_DOUBLE                   5
#define IS_STRING                   6
#define IS_ARRAY                    7
#define IS_OBJECT                   8
#define IS_RESOURCE                 9
#define IS_REFERENCE                10

// constant expressions
#define IS_CONSTANT                 11
#define IS_CONSTANT_AST             12

// internal types
#define IS_INDIRECT                 15
#define IS_PTR                      17

這個列表跟PHP5相似,但有一些內容增長:

  • IS_UNDEF類型替代了以前的NULL zval指針(注意與IS_NULL zval區分),好比在上面引用計數的例子中,變量被unset時,zval的類型就被置爲IS_UNDEF。
  • IS_BOOL類型被細分紅了IS_FALSE和IS_TRUE。由此布爾變量的值就被編碼在類型中,這就使得一些基於類型檢查的優化成爲可能。這個改變對用戶層是透明的,仍然有一個「布爾」類型。
  • 在zval上,PHP引用再也不使用is_ref標識,而是用IS_REFERENCE類型。下一部分將會討論。
  • IS_INDIRECT和IS_PTR是特殊的內部類型。

IS_LONG目前存儲的是zend_long類型的值,而不是一個普通的C語言long整數。緣由是在64位windows(LLP64)上,long型只有32位,因而在windows上PHP5的IS_LONG老是32位的。在64位操做系統上,即便你使用的是windows,PHP7都容許你使用64位的數字。

zend_refcounted類型相關的細節將在下一部分討論,如今咱們先看一下PHP引用的實現。

引用

PHP7處理PHP引用(&)的方式與PHP5徹底不一樣(我能夠告訴你這個改變是PHP7最大的bug來源之一)。PHP5中引用的實現以下:

一般,寫時複製(COW)機制意味着在修改以前,zval要先進行分離,以保證不會把其它共用該zval的變量給一塊兒修改了。這與值傳遞的語義相符。

對PHP引用來講,就不是這種狀況了。若是一個值是引用,那麼修改的時候就但願其它變量也同步被修改。PHP5用is_ref來判斷一個值是否是PHP引用,以及在修改的時候是否要執行分離操做。看一個例子:

$a = [];  // $a     -> zval_1(type=IS_ARRAY, refcount=1, is_ref=0) -> HashTable_1(value=[])
$b =& $a; // $a, $b -> zval_1(type=IS_ARRAY, refcount=2, is_ref=1) -> HashTable_1(value=[])

$b[] = 1; // $a = $b = zval_1(type=IS_ARRAY, refcount=2, is_ref=1) -> HashTable_1(value=[1])

這種設計一個很重大的問題就是不能在普通變量和PHP引用以前共享一個值。考慮以下情形:

$a = [];  // $a         -> zval_1(type=IS_ARRAY, refcount=1, is_ref=0) -> HashTable_1(value=[])
$b = $a;  // $a, $b     -> zval_1(type=IS_ARRAY, refcount=2, is_ref=0) -> HashTable_1(value=[])
$c = $b   // $a, $b, $c -> zval_1(type=IS_ARRAY, refcount=3, is_ref=0) -> HashTable_1(value=[])

$d =& $c; // $a, $b -> zval_1(type=IS_ARRAY, refcount=2, is_ref=0) -> HashTable_1(value=[])
          // $c, $d -> zval_1(type=IS_ARRAY, refcount=2, is_ref=1) -> HashTable_2(value=[])
          // $d是$c的引用, 但不是$a和$b的引用,因此zval要複製。
          //如今就有了相同的zval,一個is_ref=0,一個is_ref=1

$d[] = 1; // $a, $b -> zval_1(type=IS_ARRAY, refcount=2, is_ref=0) -> HashTable_1(value=[])
          // $c, $d -> zval_1(type=IS_ARRAY, refcount=2, is_ref=1) -> HashTable_2(value=[1])
          // 因爲有兩個獨立的zval $d[] = 1 不會修改到$a和$b.

這種行爲就致使使用PHP引用一般比普通變量更慢。下面的例子就有這個問題:

$array = range(0, 1000000);
$ref =& $array;
var_dump(count($array)); // <-- 這裏發生zval分離

由於count()的參數是按值傳遞的,而$array是一個引用變量,在把它傳遞給count()時,會對該數組執行完整的複製。若是$array不是引用,它的值就能夠共用,在傳遞的時候就不會發生複製。

如今來看下PHP7中引用的實現。因爲zval再也不是獨立分配,再也不可能使用PHP5同樣的方式。轉而增長了IS_REFERENCEl類型,它的值是以下的zend_reference結構:

struct _zend_reference {
    zend_refcounted   gc;
    zval              val;
};

因此zend_reference本質上只是一個有引用計數的zval。在一個引用集合中全部的變量都會保存一份IS_REFERENCEl類型的zval,而且指向同一個zend_reference實例。val跟其餘zval相似,特別是它能夠共享其指向的複雜值。好比數組能夠在普通變量和引用變量之間共享。

仍是上面的示例代碼,來看一下在PHP7下的情形。爲了簡潔性,我不會再寫變量的zval,只展現它們指向的值。

$a = [];  // $a                                     -> zend_array_1(refcount=1, value=[])
$b =& $a; // $a, $b -> zend_reference_1(refcount=2) -> zend_array_1(refcount=1, value=[])

$b[] = 1; // $a, $b -> zend_reference_1(refcount=2) -> zend_array_1(refcount=1, value=[1])

引用賦值會建立一個zend_reference,該引用的引用計數是2(有兩個變量用到了這個引用),可是值自己的引用計數是1(只有一個zend_reference指向了該值)。再考慮下引用變量和普通變量混合的狀況:

$a = [];  // $a         -> zend_array_1(refcount=1, value=[])
$b = $a;  // $a, $b,    -> zend_array_1(refcount=2, value=[])
$c = $b   // $a, $b, $c -> zend_array_1(refcount=3, value=[])

$d =& $c; // $a, $b                                 -> zend_array_1(refcount=3, value=[])
          // $c, $d -> zend_reference_1(refcount=2) ---^
          // 注意全部的變量共享同一個zend_array, 即便有的是引用,有的不是。

$d[] = 1; // $a, $b                                 -> zend_array_1(refcount=2, value=[])
          // $c, $d -> zend_reference_1(refcount=2) -> zend_array_2(refcount=1, value=[1])
          // 只有當賦值發生的時候,zend_array纔會複製,即寫時分離。

與PHP5一個重要的不一樣是全部的變量都能共享同一個數組,即便有的是引用變量有的不是。只有當進行修改的時候纔會發生分離。這意味着在PHP7中把一個很大的引用數組傳遞給count()是安全的,由於不會複製。可是引用仍然會比普通變量慢,由於須要分配zend_reference結構(以及由此產生的間接操做),並且機器碼處理起來也不會很快。

總結

總的來講,PHP7主要的改變是zval再也不是獨立的堆分配且其自己再也不存儲引用計數。轉而是它們指向的複雜類型的值(如字符串、數組、對象)會存儲引用計數。這一般會帶來更少的內存分配、間接操做和內存使用。

下一部分將會討論其它複雜類型。

相關文章
相關標籤/搜索