http://0x1.im/blog/php/Internal-value-representation-in-PHP-7-part-1.html
創(chuàng)新互聯(lián)建站專注骨干網(wǎng)絡(luò)服務(wù)器租用十年,服務(wù)更有保障!服務(wù)器租用,眉山服務(wù)器托管 成都服務(wù)器租用,成都服務(wù)器托管,骨干網(wǎng)絡(luò)帶寬,享受低延遲,高速訪問。靈活、實(shí)現(xiàn)低成本的共享或公網(wǎng)數(shù)據(jù)中心高速帶寬的專屬高性能服務(wù)器。
→ About → Links → Github → 公眾號
Scholer's Blog
Dec 10, 2015
本文第一部分和第二均翻譯自Nikita Popov(nikicPHP 官方開發(fā)組成員柏林科技大學(xué)的學(xué)生) 的博客。為了更符合漢語的閱讀習(xí)慣文中并不會逐字逐句的翻譯。
要理解本文你應(yīng)該對 PHP5 中變量的實(shí)現(xiàn)有了一些了解本文重點(diǎn)在于解釋 PHP7 中 zval 的變化。
由于大量的細(xì)節(jié)描述本文將會分成兩個(gè)部分第一部分主要描述 zval(zend value) 的實(shí)現(xiàn)在 PHP5 和 PHP7 中有何不同以及引用的實(shí)現(xiàn)。第二部分將會分析單獨(dú)類型strings、objects的細(xì)節(jié)。
PHP5 中 zval 結(jié)構(gòu)體定義如下
typedef struct _zval_struct { zvalue_value value; zend_uint refcount__gc; zend_uchar type; zend_uchar is_ref__gc;} zval;
如上zval 包含一個(gè) value
、一個(gè) type
以及兩個(gè) __gc
后綴的字段。value
是個(gè)聯(lián)合體用于存儲不同類型的值
typedef union _zvalue_value { long lval; // 用于 bool 類型、整型和資源類型 double dval; // 用于浮點(diǎn)類型 struct { // 用于字符串 char *val; int len; } str; HashTable *ht; // 用于數(shù)組 zend_object_value obj; // 用于對象 zend_ast *ast; // 用于常量表達(dá)式(PHP5.6 才有)} zvalue_value;
C 語言聯(lián)合體的特征是一次只有一個(gè)成員是有效的并且分配的內(nèi)存與需要內(nèi)存最多的成員匹配也要考慮內(nèi)存對齊。所有成員都存儲在內(nèi)存的同一個(gè)位置根據(jù)需要存儲不同的值。當(dāng)你需要 lval
的時(shí)候它存儲的是有符號×××需要 dval
時(shí)會存儲雙精度浮點(diǎn)數(shù)。
需要指出的是是聯(lián)合體中當(dāng)前存儲的數(shù)據(jù)類型會記錄到 type
字段用一個(gè)整型來標(biāo)記
#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 的內(nèi)存是單獨(dú)從堆heap中分配的有少數(shù)例外情況PHP 需要知道哪些 zval 是正在使用的哪些是需要釋放的。所以這就需要用到引用計(jì)數(shù)zval 中 refcount__gc
的值用于保存 zval 本身被引用的次數(shù)比如 $a = $b = 42
語句中42
被兩個(gè)變量引用所以它的引用計(jì)數(shù)就是 2。如果引用計(jì)數(shù)變成 0就意味著這個(gè)變量已經(jīng)沒有用了內(nèi)存也就可以釋放了。
注意這里提及到的引用計(jì)數(shù)指的不是 PHP 代碼中的引用使用 &
而是變量的使用次數(shù)。后面兩者需要同時(shí)出現(xiàn)時(shí)會使用『PHP 引用』和『引用』來區(qū)分兩個(gè)概念這里先忽略掉 PHP 的部分。
一個(gè)和引用計(jì)數(shù)緊密相關(guān)的概念是『寫時(shí)復(fù)制』對于多個(gè)引用來說zaval 只有在沒有變化的情況下才是共享的一旦其中一個(gè)引用改變 zval 的值就需要復(fù)制”separated”一份 zval然后修改復(fù)制后的 zval。
下面是一個(gè)關(guān)于『寫時(shí)復(fù)制』和 zval 的銷毀的例子
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)// 下面幾行是關(guān)于 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)
引用計(jì)數(shù)有個(gè)致命的問題無法檢查并釋放循環(huán)引用使用的內(nèi)存。為了解決這問題PHP 使用了循環(huán)回收的方法。當(dāng)一個(gè) zval 的計(jì)數(shù)減一時(shí)就有可能屬于循環(huán)的一部分這時(shí)將 zval 寫入到『根緩沖區(qū)』中。當(dāng)緩沖區(qū)滿時(shí)潛在的循環(huán)會被打上標(biāo)記并進(jìn)行回收。
因?yàn)橐С盅h(huán)回收實(shí)際使用的 zval 的結(jié)構(gòu)實(shí)際上如下
typedef struct _zval_gc_info { zval z; union { gc_root_buffer *buffered; struct _zval_gc_info *next; } u;} zval_gc_info;
zval_gc_info
結(jié)構(gòu)體中嵌入了一個(gè)正常的 zval 結(jié)構(gòu)同時(shí)也增加了兩個(gè)指針參數(shù)但是共屬于同一個(gè)聯(lián)合體u
所以實(shí)際使用中只有一個(gè)指針是有用的。buffered
指針用于存儲 zval 在根緩沖區(qū)的引用地址所以如果在循環(huán)回收執(zhí)行之前 zval 已經(jīng)被銷毀了這個(gè)字段就可能被移除了。next
在回收銷毀值的時(shí)候使用這里不會深入。
下面說說關(guān)于內(nèi)存使用上的情況這里說的都是指在 64 位的系統(tǒng)上。首先由于 str
和 obj
占用的大小一樣 zvalue_value
這個(gè)聯(lián)合體占用 16 個(gè)字節(jié)bytes的內(nèi)存。整個(gè) zval
結(jié)構(gòu)體占用的內(nèi)存是 24 個(gè)字節(jié)考慮到內(nèi)存對齊zval_gc_info
的大小是 32 個(gè)字節(jié)。綜上在堆相對于棧分配給 zval 的內(nèi)存需要額外的 16 個(gè)字節(jié)所以每個(gè) zval 在不同的地方一共需要用到 48 個(gè)字節(jié)要理解上面的計(jì)算方式需要注意每個(gè)指針在 64 位的系統(tǒng)上也需要占用 8 個(gè)字節(jié)。
在這點(diǎn)上不管從什么方面去考慮都可以認(rèn)為 zval 的這種設(shè)計(jì)效率是很低的。比如 zval 在存儲整型的時(shí)候本身只需要 8 個(gè)字節(jié)即使考慮到需要存一些附加信息以及內(nèi)存對齊額外 8 個(gè)字節(jié)應(yīng)該也是足夠的。
在存儲整型時(shí)本來確實(shí)需要 16 個(gè)字節(jié)但是實(shí)際上還有 16 個(gè)字節(jié)用于引用計(jì)數(shù)、16 個(gè)字節(jié)用于循環(huán)回收。所以說 zval 的內(nèi)存分配和釋放都是消耗很大的操作我們有必要對其進(jìn)行優(yōu)化。
從這個(gè)角度思考一個(gè)整型數(shù)據(jù)真的需要存儲引用計(jì)數(shù)、循環(huán)回收的信息并且單獨(dú)在堆上分配內(nèi)存嗎答案是當(dāng)然不這種處理方式一點(diǎn)都不好。
這里總結(jié)一下 PHP5 中 zval 實(shí)現(xiàn)方式存在的主要問題
zval 總是單獨(dú)從堆中分配內(nèi)存
zval 總是存儲引用計(jì)數(shù)和循環(huán)回收的信息即使是整型這種可能并不需要此類信息的數(shù)據(jù)
在使用對象或者資源時(shí)直接引用會導(dǎo)致兩次計(jì)數(shù)原因會在下一部分講
某些間接訪問需要一個(gè)更好的處理方式。比如現(xiàn)在訪問存儲在變量中的對象間接使用了四個(gè)指針指針鏈的長度為四。這個(gè)問題也放到下一部分討論
直接計(jì)數(shù)也就意味著數(shù)值只能在 zval 之間共享。如果想在 zval 和 hashtable key 之間共享一個(gè)字符串就不行除非 hashtable key 也是 zval。
在 PHP7 中 zval 有了新的實(shí)現(xiàn)方式。最基礎(chǔ)的變化就是 zval 需要的內(nèi)存不再是單獨(dú)從堆上分配不再自己存儲引用計(jì)數(shù)。復(fù)雜數(shù)據(jù)類型比如字符串、數(shù)組和對象的引用計(jì)數(shù)由其自身來存儲。這種實(shí)現(xiàn)方式有以下好處
簡單數(shù)據(jù)類型不需要單獨(dú)分配內(nèi)存也不需要計(jì)數(shù)
不會再有兩次計(jì)數(shù)的情況。在對象中只有對象自身存儲的計(jì)數(shù)是有效的
由于現(xiàn)在計(jì)數(shù)由數(shù)值自身存儲所以也就可以和非 zval 結(jié)構(gòu)的數(shù)據(jù)共享比如 zval 和 hashtable key 之間
間接訪問需要的指針數(shù)減少了。
我們看看現(xiàn)在 zval 結(jié)構(gòu)體的定義現(xiàn)在在 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;};
結(jié)構(gòu)體的第一個(gè)元素沒太大變化仍然是一個(gè) value
聯(lián)合體。第二個(gè)成員是由一個(gè)表示類型信息的整型和一個(gè)包含四個(gè)字符變量的結(jié)構(gòu)體組成的聯(lián)合體可以忽略 ZEND_ENDIAN_LOHI_4
宏它只是用來解決跨平臺大小端問題的。這個(gè)子結(jié)構(gòu)中比較重要的部分是 type
和以前類似和 type_flags
這個(gè)接下來會解釋。
上面這個(gè)地方也有一點(diǎn)小問題value
本來應(yīng)該占 8 個(gè)字節(jié)但是由于內(nèi)存對齊哪怕只增加一個(gè)字節(jié)實(shí)際上也是占用 16 個(gè)字節(jié)使用一個(gè)字節(jié)就意味著需要額外的 8 個(gè)字節(jié)。但是顯然我們并不需要 8 個(gè)字節(jié)來存儲一個(gè) type 字段所以我們在 u1
的后面增加了了一個(gè)名為 u2
的聯(lián)合體。默認(rèn)情況下是用不到的需要使用的時(shí)候可以用來存儲 4 個(gè)字節(jié)的數(shù)據(jù)。這個(gè)聯(lián)合體可以滿足不同場景下的需求。
PHP7 中 value
的結(jié)構(gòu)定義如下
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;
首先需要注意的是現(xiàn)在 value 聯(lián)合體需要的內(nèi)存是 8 個(gè)字節(jié)而不是 16。它只會直接存儲整型lval
或者浮點(diǎn)型dval
數(shù)據(jù)其他情況下都是指針上面提到過指針占用 8 個(gè)字節(jié)最下面的結(jié)構(gòu)體由兩個(gè) 4 字節(jié)的無符號整型組成。上面所有的指針類型除了特殊標(biāo)記的都有一個(gè)同樣的頭zend_refcounted
用來存儲引用計(jì)數(shù)
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;
現(xiàn)在這個(gè)結(jié)構(gòu)體肯定會包含一個(gè)存儲引用計(jì)數(shù)的字段。除此之外還有 type
、flags
和 gc_info
。type
存儲的和 zval 中的 type 相同的內(nèi)容這樣 GC 在不存儲 zval 的情況下單獨(dú)使用引用計(jì)數(shù)。flags
在不同的數(shù)據(jù)類型中有不同的用途這個(gè)放到下一部分講。
gc_info
和 PHP5 中的 buffered
作用相同不過不再是位于根緩沖區(qū)的指針而是一個(gè)索引數(shù)字。因?yàn)橐郧案彌_區(qū)的大小是固定的10000 個(gè)元素所以使用一個(gè) 16 位2 字節(jié)的數(shù)字代替 64 位8 字節(jié)的指針足夠了。gc_info
中同樣包含一個(gè)『顏色』位用于回收時(shí)標(biāo)記結(jié)點(diǎn)。
上文提到過 zval 需要的內(nèi)存不再單獨(dú)從堆上分配。但是顯然總要有地方來存儲它所以會存在哪里呢實(shí)際上大多時(shí)候它還是位于堆中所以前文中提到的地方重點(diǎn)不是堆
而是單獨(dú)分配
只不過是嵌入到其他的數(shù)據(jù)結(jié)構(gòu)中的比如 hashtable 和 bucket 現(xiàn)在就會直接有一個(gè) zval 字段而不是指針。所以函數(shù)表編譯變量和對象屬性在存儲時(shí)會是一個(gè) zval 數(shù)組并得到一整塊內(nèi)存而不是散落在各處的 zval 指針。之前的 zval *
現(xiàn)在都變成了 zval
。
之前當(dāng) zval 在一個(gè)新的地方使用時(shí)會復(fù)制一份 zval *
并增加一次引用計(jì)數(shù)。現(xiàn)在就直接復(fù)制 zval 的值忽略 u2
某些情況下可能會增加其結(jié)構(gòu)指針指向的引用計(jì)數(shù)如果在進(jìn)行計(jì)數(shù)。
那么 PHP 怎么知道 zval 是否正在計(jì)數(shù)呢不是所有的數(shù)據(jù)類型都能知道因?yàn)橛行╊愋捅热缱址驍?shù)組并不是總需要進(jìn)行引用計(jì)數(shù)。所以 type_info
字段就是用來記錄 zval 是否在進(jìn)行計(jì)數(shù)的這個(gè)字段的值有以下幾種情況
#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 的正式版本中上面這一段宏定義的注釋這幾個(gè)宏是供 zval.u1.v.type_flags
使用的。這應(yīng)該是注釋的錯(cuò)誤因?yàn)檫@個(gè)上述字段是 zend_uchar
類型。
type_info
的三個(gè)主要的屬性就是『可計(jì)數(shù)』refcounted、『可回收』collectable和『可復(fù)制』copyable。計(jì)數(shù)的問題上面已經(jīng)提過了。『可回收』用于標(biāo)記 zval 是否參與循環(huán)不如字符串通常是可計(jì)數(shù)的但是你卻沒辦法給字符串制造一個(gè)循環(huán)引用的情況。
是否可復(fù)制用于表示在復(fù)制時(shí)是否需要在復(fù)制時(shí)制造原文用的 “duplication” 來表述用中文表達(dá)出來可能不是很好理解一份一模一樣的實(shí)體。”duplication” 屬于深度復(fù)制比如在復(fù)制數(shù)組時(shí)不僅僅是簡單增加數(shù)組的引用計(jì)數(shù)而是制造一份全新值一樣的數(shù)組。但是某些類型比如對象和資源即使 “duplication” 也只能是增加引用計(jì)數(shù)這種就屬于不可復(fù)制的類型。這也和對象和資源現(xiàn)有的語義匹配現(xiàn)有PHP7 也是這樣不單是 PHP5。
下面的表格上標(biāo)明了不同的類型會使用哪些標(biāo)記x
標(biāo)記的都是有的特性。『簡單類型』simple types指的是整型或布爾類型這些不使用指針指向一個(gè)結(jié)構(gòu)體的類型。下表中也有『不可變』immutable的標(biāo)記它用來標(biāo)記不可變數(shù)組的這個(gè)在下一部分再詳述。
interned string保留字符在這之前沒有提過其實(shí)就是函數(shù)名、變量名等無需計(jì)數(shù)、不可重復(fù)的字符串。
| 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 | | |
要理解這一點(diǎn)我們可以來看幾個(gè)例子這樣可以更好的認(rèn)識 zval 內(nèi)存管理是怎么工作的。
下面是整數(shù)行為模式在上文中 PHP5 的例子的基礎(chǔ)上進(jìn)行了一些簡化
這個(gè)過程其實(shí)挺簡單的。現(xiàn)在整數(shù)不再是共享的變量直接就會分離成兩個(gè)單獨(dú)的 zval由于現(xiàn)在 zval 是內(nèi)嵌的所以也不需要單獨(dú)分配內(nèi)存所以這里的注釋中使用
=
來表示的而不是指針符號->
unset 時(shí)變量會被標(biāo)記為IS_UNDEF
。下面看一下更復(fù)雜的情況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 分離在這里進(jìn)行$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=[])這種情況下每個(gè)變量變量有一個(gè)單獨(dú)的 zval但是是指向同一個(gè)有引用計(jì)數(shù)
zend_array
的結(jié)構(gòu)體。修改其中一個(gè)數(shù)組的值時(shí)才會進(jìn)行復(fù)制。這點(diǎn)和 PHP5 的情況類似。類型Types
我們大概看一下 PHP7 支持哪些類型zval 使用的類型標(biāo)記
/* 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這個(gè)列表和 PHP5 使用的類似不過增加了幾項(xiàng)
IS_UNDEF
用來標(biāo)記之前為 NULL
的 zval 指針和 IS_NULL
并不沖突。比如在上面的例子中使用unset
注銷變量
IS_BOOL
現(xiàn)在分割成了 IS_FALSE
和 IS_TRUE
兩項(xiàng)。現(xiàn)在布爾類型的標(biāo)記是直接記錄到 type 中這么做可以優(yōu)化類型檢查。不過這個(gè)變化對用戶是透明的還是只有一個(gè)『布爾』類型的數(shù)據(jù)PHP 腳本中。
PHP 引用不再使用 is_ref
來標(biāo)記而是使用 IS_REFERENCE
類型。這個(gè)也要放到下一部分講
IS_INDIRECT
和 IS_PTR
是特殊的內(nèi)部標(biāo)記。
實(shí)際上上面的列表中應(yīng)該還存在兩個(gè) fake types這里忽略了。
IS_LONG
類型表示的是一個(gè) zend_long
的值而不是原生的 C 語言的 long 類型。原因是 Windows 的 64 位系統(tǒng)LLP64上的 long
類型只有 32 位的位深度。所以 PHP5 在 Windows 上只能使用 32 位的數(shù)字。PHP7 允許你在 64 位的操作系統(tǒng)上使用 64 位的數(shù)字即使是在 Windows 上面也可以。
zend_refcounted
的內(nèi)容會在下一部分講。下面看看 PHP 引用的實(shí)現(xiàn)。
PHP7 使用了和 PHP5 中完全不同的方法來處理 PHP &
符號引用的問題這個(gè)改動也是 PHP7 開發(fā)過程中大量 bug 的根源。我們先從 PHP5 中 PHP 引用的實(shí)現(xiàn)方式說起。
通常情況下 寫時(shí)復(fù)制原則意味著當(dāng)你修改一個(gè) zval 之前需要對其進(jìn)行分離來保證始終修改的只是某一個(gè) PHP 變量的值。這就是傳值調(diào)用的含義。
但是使用 PHP 引用時(shí)這條規(guī)則就不適用了。如果一個(gè) PHP 變量是 PHP 引用就意味著你想要在將多個(gè) PHP 變量指向同一個(gè)值。PHP5 中的 is_ref
標(biāo)記就是用來注明一個(gè) PHP 變量是不是 PHP 引用在修改時(shí)需不需要進(jìn)行分離的。比如
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]) // 因?yàn)?nbsp;is_ref 的值是 1, 所以 PHP 不會對 zval 進(jìn)行分離
但是這個(gè)設(shè)計(jì)的一個(gè)很大的問題在于它無法在一個(gè) PHP 引用變量和 PHP 非引用變量之間共享同一個(gè)值。比如下面這種情況
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 還是需要進(jìn)行復(fù)制 // 這樣我們就有了兩個(gè) zval, 一個(gè) is_ref 的值是 0, 一個(gè) 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]) // 因?yàn)橛袃蓚€(gè)分離了的 zval, $d[] = 1 的語句就不會修改 $a 和 $b 的值.
這種行為方式也導(dǎo)致在 PHP 中使用引用比普通的值要慢。比如下面這個(gè)例子
因?yàn)?nbsp;
count()
只接受傳值調(diào)用但是$array
是一個(gè) PHP 引用所以count()
在執(zhí)行之前實(shí)際上會有一個(gè)對數(shù)組進(jìn)行完整的復(fù)制的過程。如果$array
不是引用這種情況就不會發(fā)生了。現(xiàn)在我們來看看 PHP7 中 PHP 引用的實(shí)現(xiàn)。因?yàn)?zval 不再單獨(dú)分配內(nèi)存也就沒辦法再使用和 PHP5 中相同的實(shí)現(xiàn)了。所以增加了一個(gè)
IS_REFERENCE
類型并且專門使用zend_reference
來存儲引用值struct _zend_reference { zend_refcounted gc; zval val;};本質(zhì)上
zend_reference
只是增加了引用計(jì)數(shù)的 zval。所有引用變量都會存儲一個(gè) zval 指針并且被標(biāo)記為IS_REFERENCE
。val
和其他的 zval 的行為一樣尤其是它也可以在共享其所存儲的復(fù)雜變量的指針比如數(shù)組可以在引用變量和值變量之間共享。我們還是看例子這次是 PHP7 中的語義。為了簡潔明了這里不再單獨(dú)寫出 zval只展示它們指向的結(jié)構(gòu)體
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])上面的例子中進(jìn)行引用傳遞時(shí)會創(chuàng)建一個(gè)
zend_reference
注意它的引用計(jì)數(shù)是 2因?yàn)橛袃蓚€(gè)變量在使用這個(gè) PHP 引用。但是值本身的引用計(jì)數(shù)是 1因?yàn)?nbsp;zend_reference
只是有一個(gè)指針指向它。下面看看引用和非引用混合的情況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) ---^ // 注意所有變量共享同一個(gè) 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]) // 只有在這時(shí)進(jìn)行賦值的時(shí)候才會對 zend_array 進(jìn)行賦值這里和 PHP5 最大的不同就是所有的變量都可以共享同一個(gè)數(shù)組即使有的是 PHP 引用有的不是。只有當(dāng)其中某一部分被修改的時(shí)候才會對數(shù)組進(jìn)行分離。這也意味著使用
count()
時(shí)即使給其傳遞一個(gè)很大的引用數(shù)組也是安全的不會再進(jìn)行復(fù)制。不過引用仍然會比普通的數(shù)值慢因?yàn)榇嬖谛枰獮?nbsp;zend_reference
結(jié)構(gòu)體分配內(nèi)存間接并且引擎本身處理這一塊兒也不快的的原因。結(jié)語
總結(jié)一下 PHP7 中最重要的改變就是 zval 不再單獨(dú)從堆上分配內(nèi)存并且不自己存儲引用計(jì)數(shù)。需要使用 zval 指針的復(fù)雜類型比如字符串、數(shù)組和對象會自己存儲引用計(jì)數(shù)。這樣就可以有更少的內(nèi)存分配操作、更少的間接指針使用以及更少的內(nèi)存分配。
文章的第二部分我們會討論復(fù)雜類型的問題。
More:
Jan 07,2017 再見2016我在騰訊這一年
Jan 02,2017 如何學(xué)習(xí) PHP 源碼 - 從編譯開始
Website powered by Jekyll, hosted on Github and theme of marcgg
All of the blog's articles are under Creative commons license unless stated otherwise. Everything else is .