本文第一部分和第二均翻譯自Nikita Popov(nikic,PHP 官方開發組成員,柏林科技大學的學生) 的博客。爲了更符合漢語的閱讀習慣,文中並不會逐字逐句的翻譯。php
要理解本文,你應該對 PHP5 中變量的實現有了一些瞭解,本文重點在於解釋 PHP7 中 zval 的變化。html
因爲大量的細節描述,本文將會分紅兩個部分:第一部分主要描述 zval(zend value) 的實如今 PHP5 和 PHP7 中有何不一樣以及引用的實現。第二部分將會分析單獨類型(strings、objects)的細節。node
PHP5 中 zval 結構體定義以下:git
typedef struct _zval_struct { zvalue_value value; zend_uint refcount__gc; zend_uchar type; zend_uchar is_ref__gc; } zval;
如上,zval 包含一個 value
、一個 type
以及兩個 __gc
後綴的字段。value
是個聯合體,用於存儲不一樣類型的值:github
typedef union _zvalue_value { long lval; // 用於 bool 類型、整型和資源類型 double dval; // 用於浮點類型 struct { // 用於字符串 char *val; int len; } str; HashTable *ht; // 用於數組 zend_object_value obj; // 用於對象 zend_ast *ast; // 用於常量表達式(PHP5.6 纔有) } zvalue_value;
C 語言聯合體的特徵是一次只有一個成員是有效的而且分配的內存與須要內存最多的成員匹配(也要考慮內存對齊)。全部成員都存儲在內存的同一個位置,根據須要存儲不一樣的值。當你須要 lval
的時候,它存儲的是有符號整形,須要 dval
時,會存儲雙精度浮點數。express
須要指出的是是聯合體中當前存儲的數據類型會記錄到 type
字段,用一個整型來標記:數組
#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中,zval 的內存是單獨從堆(heap)中分配的(有少數例外狀況),PHP 須要知道哪些 zval 是正在使用的,哪些是須要釋放的。因此這就須要用到引用計數:zval 中 refcount__gc
的值用於保存 zval 自己被引用的次數,好比 $a = $b = 42
語句中,42
被兩個變量引用,因此它的引用計數就是 2。若是引用計數變成 0,就意味着這個變量已經沒有用了,內存也就能夠釋放了。安全
注意這裏說起到的引用計數指的不是 PHP 代碼中的引用(使用 &
),而是變量的使用次數。後面二者須要同時出現時會使用『PHP 引用』和『引用』來區分兩個概念,這裏先忽略掉 PHP 的部分。數據結構
一個和引用計數緊密相關的概念是『寫時複製』:對於多個引用來講,zaval 只有在沒有變化的狀況下才是共享的,一旦其中一個引用改變 zval 的值,就須要複製("separated")一份 zval,而後修改複製後的 zval。函數
下面是一個關於『寫時複製』和 zval 的銷燬的例子:
<?php $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 is destroyed, because 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 位的系統上。首先,因爲 str
和 obj
佔用的大小同樣, zvalue_value
這個聯合體佔用 16 個字節(bytes)的內存。整個 zval
結構體佔用的內存是 24 個字節(考慮到內存對齊),zval_gc_info
的大小是 32 個字節。綜上,在堆(相對於棧)分配給 zval 的內存須要額外的 16 個字節,因此每一個 zval 在不一樣的地方一共須要用到 48 個字節(要理解上面的計算方式須要注意每一個指針在 64 位的系統上也須要佔用 8 個字節)。
在這點上無論從什麼方面去考慮均可以認爲 zval 的這種設計效率是很低的。好比 zval 在存儲整型的時候自己只須要 8 個字節,即便考慮到須要存一些附加信息以及內存對齊,額外 8 個字節應該也是足夠的。
在存儲整型時原本確實須要 16 個字節,可是實際上還有 16 個字節用於引用計數、16 個字節用於循環回收。因此說 zval 的內存分配和釋放都是消耗很大的操做,咱們有必要對其進行優化。
從這個角度思考:一個整型數據真的須要存儲引用計數、循環回收的信息而且單獨在堆上分配內存嗎?答案是固然不,這種處理方式一點都很差。
這裏總結一下 PHP5 中 zval 實現方式存在的主要問題:
zval 老是單獨從堆中分配內存;
zval 老是存儲引用計數和循環回收的信息,即便是整型這種可能並不須要此類信息的數據;
在使用對象或者資源時,直接引用會致使兩次計數(緣由會在下一部分講);
某些間接訪問須要一個更好的處理方式。好比如今訪問存儲在變量中的對象間接使用了四個指針(指針鏈的長度爲四)。這個問題也放到下一部分討論;
直接計數也就意味着數值只能在 zval 之間共享。若是想在 zval 和 hashtable key 之間共享一個字符串就不行(除非 hashtable key 也是 zval)。
在 PHP7 中 zval 有了新的實現方式。最基礎的變化就是 zval 須要的內存再也不是單獨從堆上分配,再也不本身存儲引用計數。複雜數據類型(好比字符串、數組和對象)的引用計數由其自身來存儲。這種實現方式有如下好處:
簡單數據類型不須要單獨分配內存,也不須要計數;
不會再有兩次計數的狀況。在對象中,只有對象自身存儲的計數是有效的;
因爲如今計數由數值自身存儲,因此也就能夠和非 zval 結構的數據共享,好比 zval 和 hashtable key 之間;
間接訪問須要的指針數減小了。
咱們看看如今 zval 結構體的定義(如今在 zend_types.h 文件中):
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 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 個字節,可是因爲內存對齊,哪怕只增長一個字節,實際上也是佔用 16 個字節(使用一個字節就意味着須要額外的 8 個字節)。可是顯然咱們並不須要 8 個字節來存儲一個 type 字段,因此咱們在 u1
的後面增長了了一個名爲 u2
的聯合體。默認狀況下是用不到的,須要使用的時候能夠用來存儲 4 個字節的數據。這個聯合體能夠知足不一樣場景下的需求。
PHP7 中 value
的結構定義以下:
typedef union _zend_value { zend_long lval; /* long value */ double dval; /* double value */ zend_refcounted *counted; zend_string *str; zend_array *arr; zend_object *obj; zend_resource *res; zend_reference *ref; zend_ast_ref *ast; zval *zv; void *ptr; zend_class_entry *ce; zend_function *func; struct { uint32_t w1; uint32_t w2; } ww; } zend_value;
首先須要注意的是如今 value 聯合體須要的內存是 8 個字節而不是 16。它只會直接存儲整型(lval
)或者浮點型(dval
)數據,其餘狀況下都是指針(上面提到過,指針佔用 8 個字節,最下面的結構體由兩個 4 字節的無符號整型組成)。上面全部的指針類型(除了特殊標記的)都有一個一樣的頭(zend_refcounted
)用來存儲引用計數:
typedef struct _zend_refcounted_h { uint32_t refcount; /* reference counter 32-bit */ union { struct { ZEND_ENDIAN_LOHI_3( zend_uchar type, zend_uchar flags, /* used for strings & objects */ uint16_t gc_info) /* keeps GC root number (or 0) and color */ } v; uint32_t type_info; } u; } zend_refcounted_h;
如今,這個結構體確定會包含一個存儲引用計數的字段。除此以外還有 type
、flags
和 gc_info
。type
存儲的和 zval 中的 type 相同的內容,這樣 GC 在不存儲 zval 的狀況下單獨使用引用計數。flags
在不一樣的數據類型中有不一樣的用途,這個放到下一部分講。
gc_info
和 PHP5 中的 buffered
做用相同,不過再也不是位於根緩衝區的指針,而是一個索引數字。由於之前根緩衝區的大小是固定的(10000 個元素),因此使用一個 16 位(2 字節)的數字代替 64 位(8 字節)的指針足夠了。gc_info
中一樣包含一個『顏色』位用於回收時標記結點。
上文提到過 zval 須要的內存再也不單獨從堆上分配。可是顯然總要有地方來存儲它,因此會存在哪裏呢?實際上大多時候它仍是位於堆中(因此前文中提到的地方重點不是堆
,而是單獨分配
),只不過是嵌入到其餘的數據結構中的,好比 hashtable 和 bucket 如今就會直接有一個 zval 字段而不是指針。因此函數表編譯變量和對象屬性在存儲時會是一個 zval 數組並獲得一整塊內存而不是散落在各處的 zval 指針。以前的 zval *
如今都變成了 zval
。
以前當 zval 在一個新的地方使用時會複製一份 zval *
並增長一次引用計數。如今就直接複製 zval 的值(忽略 u2
),某些狀況下可能會增長其結構指針指向的引用計數(若是在進行計數)。
那麼 PHP 怎麼知道 zval 是否正在計數呢?不是全部的數據類型都能知道,由於有些類型(好比字符串或數組)並非總須要進行引用計數。因此 type_info
字段就是用來記錄 zval 是否在進行計數的,這個字段的值有如下幾種狀況:
#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 */
注:在 7.0.0 的正式版本中,上面這一段宏定義的註釋這幾個宏是供 zval.u1.v.type_flags
使用的。這應該是註釋的錯誤,由於這個上述字段是 zend_uchar
類型。
type_info
的三個主要的屬性就是『可計數』(refcounted)、『可回收』(collectable)和『可複製』(copyable)。計數的問題上面已經提過了。『可回收』用於標記 zval 是否參與循環,不如字符串一般是可計數的,可是你卻沒辦法給字符串製造一個循環引用的狀況。
是否可複製用於表示在複製時是否須要在複製時製造(原文用的 "duplication" 來表述,用中文表達出來可能不是很好理解)一份如出一轍的實體。"duplication" 屬於深度複製,好比在複製數組時,不只僅是簡單增長數組的引用計數,而是製造一份全新值同樣的數組。可是某些類型(好比對象和資源)即便 "duplication" 也只能是增長引用計數,這種就屬於不可複製的類型。這也和對象和資源現有的語義匹配(現有,PHP7 也是這樣,不單是 PHP5)。
下面的表格上標明瞭不一樣的類型會使用哪些標記(x
標記的都是有的特性)。『簡單類型』(simple types)指的是整型或布爾類型這些不使用指針指向一個結構體的類型。下表中也有『不可變』(immutable)的標記,它用來標記不可變數組的,這個在下一部分再詳述。
interned string(保留字符)在這以前沒有提過,其實就是函數名、變量名等無需計數、不可重複的字符串。
| 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 的例子的基礎上進行了一些簡化 :
<?php $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 時變量會被標記爲 IS_UNDEF
。下面看一下更復雜的狀況:
<?php $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), zend_array_2 被銷燬 // $b = zval_2(type=IS_ARRAY) -> zend_array_1(refcount=1, value=[])
這種狀況下每一個變量變量有一個單獨的 zval,可是是指向同一個(有引用計數) zend_array
的結構體。修改其中一個數組的值時纔會進行復制。這點和 PHP5 的狀況相似。
咱們大概看一下 PHP7 支持哪些類型(zval 使用的類型標記):
/* 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
並不衝突)。好比在上面的例子中使用 unset
註銷變量;
IS_BOOL
如今分割成了 IS_FALSE
和 IS_TRUE
兩項。如今布爾類型的標記是直接記錄到 type 中,這麼作能夠優化類型檢查。不過這個變化對用戶是透明的,仍是隻有一個『布爾』類型的數據(PHP 腳本中)。
PHP 引用再也不使用 is_ref
來標記,而是使用 IS_REFERENCE
類型。這個也要放到下一部分講;
IS_INDIRECT
和 IS_PTR
是特殊的內部標記。
實際上上面的列表中應該還存在兩個 fake types,這裏忽略了。
IS_LONG
類型表示的是一個 zend_long
的值,而不是原生的 C 語言的 long 類型。緣由是 Windows 的 64 位系統(LLP64)上的 long
類型只有 32 位的位深度。因此 PHP5 在 Windows 上只能使用 32 位的數字。PHP7 容許你在 64 位的操做系統上使用 64 位的數字,即便是在 Windows 上面也能夠。
zend_refcounted
的內容會在下一部分講。下面看看 PHP 引用的實現。
PHP7 使用了和 PHP5 中徹底不一樣的方法來處理 PHP &
符號引用的問題(這個改動也是 PHP7 開發過程當中大量 bug 的根源)。咱們先從 PHP5 中 PHP 引用的實現方式提及。
一般狀況下, 寫時複製原則意味着當你修改一個 zval 以前須要對其進行分離來保證始終修改的只是某一個 PHP 變量的值。這就是傳值調用的含義。
可是使用 PHP 引用時這條規則就不適用了。若是一個 PHP 變量是 PHP 引用,就意味着你想要在將多個 PHP 變量指向同一個值。PHP5 中的 is_ref
標記就是用來註明一個 PHP 變量是否是 PHP 引用,在修改時需不須要進行分離的。好比:
<?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]) // 由於 is_ref 的值是 1, 因此 PHP 不會對 zval 進行分離
可是這個設計的一個很大的問題在於它沒法在一個 PHP 引用變量和 PHP 非引用變量之間共享同一個值。好比下面這種狀況:
<?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 中使用引用比普通的值要慢。好比下面這個例子:
<?php $array = range(0, 1000000); $ref =& $array; var_dump(count($array)); // <-- 這裏會進行分離
由於 count()
只接受傳值調用,可是 $array
是一個 PHP 引用,因此 count()
在執行以前實際上會有一個對數組進行完整的複製的過程。若是 $array
不是引用,這種狀況就不會發生了。
如今咱們來看看 PHP7 中 PHP 引用的實現。由於 zval 再也不單獨分配內存,也就沒辦法再使用和 PHP5 中相同的實現了。因此增長了一個 IS_REFERENCE
類型,而且專門使用 zend_reference
來存儲引用值:
struct _zend_reference { zend_refcounted gc; zval val; };
本質上 zend_reference
只是增長了引用計數的 zval。全部引用變量都會存儲一個 zval 指針而且被標記爲 IS_REFERENCE
。val
和其餘的 zval 的行爲同樣,尤爲是它也能夠在共享其所存儲的複雜變量的指針,好比數組能夠在引用變量和值變量之間共享。
咱們仍是看例子,此次是 PHP7 中的語義。爲了簡潔明瞭這裏再也不單獨寫出 zval,只展現它們指向的結構體:
<?php $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(由於有兩個變量在使用這個 PHP 引用)。可是值自己的引用計數是 1(由於 zend_reference
只是有一個指針指向它)。下面看看引用和非引用混合的狀況:
<?php $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, 即便有的是 PHP 引用有的不是 $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 最大的不一樣就是全部的變量均可以共享同一個數組,即便有的是 PHP 引用有的不是。只有當其中某一部分被修改的時候纔會對數組進行分離。這也意味着使用 count()
時即便給其傳遞一個很大的引用數組也是安全的,不會再進行復制。不過引用仍然會比普通的數值慢,由於存在須要爲 zend_reference
結構體分配內存(間接)而且引擎自己處理這一起也不快的的緣由。
總結一下 PHP7 中最重要的改變就是 zval 再也不單獨從堆上分配內存而且不本身存儲引用計數。須要使用 zval 指針的複雜類型(好比字符串、數組和對象)會本身存儲引用計數。這樣就能夠有更少的內存分配操做、更少的間接指針使用以及更少的內存分配。
文章的第二部分咱們會討論複雜類型的問題。
私博地址:http://0x1.im