《PHP學(xué)習(xí):變量在 PHP7 內(nèi)部的實(shí)現(xiàn)(一)》要點(diǎn):
本文介紹了PHP學(xué)習(xí):變量在 PHP7 內(nèi)部的實(shí)現(xiàn)(一),希望對(duì)您有用。如果有疑問(wèn),可以聯(lián)系我們。
<?php $array = range(0, 1000000); $ref =& $array; var_dump(count($array)); // <-- 這里會(huì)進(jìn)行分離
由于大量的細(xì)節(jié)描述,本文將會(huì)分成兩個(gè)部分:第一部分主要描述 zval(zend value) 的實(shí)現(xiàn)在 PHP5 和 PHP7 中有何分歧以及引用的實(shí)現(xiàn).第二部分將會(huì)分析單獨(dú)類型(strings、objects)的細(xì)節(jié).PHP應(yīng)用
PHP5 中的 zvalPHP應(yīng)用
PHP5 中 zval 布局體定義如下:PHP應(yīng)用
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)合體,用于存儲(chǔ)不同類型的值:PHP應(yīng)用
typedef union _zvalue_value { long lval; // 用于 bool 類型、整型和資源類型 double dval; // 用于浮點(diǎn)類型 struct { // 用于字符串 char *val; int len; } str; HashTable *ht; // 用于數(shù)組 zend_object_value obj; // 用于對(duì)象 zend_ast *ast; // 用于常量表達(dá)式(PHP5.6 才有) } zvalue_value;
C 語(yǔ)言聯(lián)合體的特征是一次只有一個(gè)成員是有效的并且分配的內(nèi)存與必要內(nèi)存最多的成員匹配(也要考慮內(nèi)存對(duì)齊).所有成員都存儲(chǔ)在內(nèi)存的同一個(gè)位置,根據(jù)必要存儲(chǔ)不同的值.當(dāng)你必要 lval 的時(shí)候,它存儲(chǔ)的是有符號(hào)整形,必要 dval 時(shí),會(huì)存儲(chǔ)雙精度浮點(diǎn)數(shù).PHP應(yīng)用
必要指出的是是聯(lián)合體中當(dāng)前存儲(chǔ)的數(shù)據(jù)類型會(huì)記錄到 type 字段,用一個(gè)整型來(lái)標(biāo)記:PHP應(yīng)用
#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 9PHP應(yīng)用
PHP5 中的引用計(jì)數(shù)PHP應(yīng)用
在PHP5中,zval 的內(nèi)存是單獨(dú)從堆(heap)中分配的(有少數(shù)例外情況),PHP 需要知道哪些 zval 是正在使用的,哪些是需要釋放的.所以這就需要用到引用計(jì)數(shù):zval 中 refcount__gc 的值用于保留 zval 本身被引用的次數(shù),比如 $a = $b = 42 語(yǔ)句中,42 被兩個(gè)變量引用,所以它的引用計(jì)數(shù)就是 2.如果引用計(jì)數(shù)變成 0,就意味著這個(gè)變量已經(jīng)沒(méi)有用了,內(nèi)存也就可以釋放了.PHP應(yīng)用
注意這里提及到的引用計(jì)數(shù)指的不是 PHP 代碼中的引用(使用 &),而是變量的使用次數(shù).后面兩者必要同時(shí)出現(xiàn)時(shí)會(huì)使用『PHP 引用』和『引用』來(lái)區(qū)分兩個(gè)概念,這里先忽略掉 PHP 的部分.PHP應(yīng)用
一個(gè)和引用計(jì)數(shù)緊密相關(guān)的概念是『寫時(shí)復(fù)制』:對(duì)于多個(gè)引用來(lái)說(shuō),zaval 只有在沒(méi)有變化的情況下才是共享的,一旦其中一個(gè)引用改變 zval 的值,就必要復(fù)制("separated")一份 zval,然后修改復(fù)制后的 zval.PHP應(yīng)用
下面是一個(gè)關(guān)于『寫時(shí)復(fù)制』和 zval 的銷毀的例子:PHP應(yīng)用
<?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) // 下面幾行是關(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è)致命的問(wèn)題:無(wú)法檢查并釋放循環(huán)引用(使用的內(nèi)存).為了解決這問(wèn)題,PHP 使用了循環(huán)回收的辦法.當(dāng)一個(gè) zval 的計(jì)數(shù)減一時(shí),就有可能屬于循環(huán)的一部分,這時(shí)將 zval 寫入到『根緩沖區(qū)』中.當(dāng)緩沖區(qū)滿時(shí),潛在的循環(huán)會(huì)被打上標(biāo)記并進(jìn)行回收.PHP應(yīng)用
因?yàn)橐С盅h(huán)回收,實(shí)際使用的 zval 的布局實(shí)際上如下:PHP應(yīng)用
typedef struct _zval_gc_info { zval z; union { gc_root_buffer *buffered; struct _zval_gc_info *next; } u; } zval_gc_info;
zval_gc_info 布局體中嵌入了一個(gè)正常的 zval 布局,同時(shí)也增加了兩個(gè)指針參數(shù),但是共屬于同一個(gè)聯(lián)合體 u,所以實(shí)際使用中只有一個(gè)指針是有用的.buffered 指針用于存儲(chǔ) zval 在根緩沖區(qū)的引用地址,所以如果在循環(huán)回收?qǐng)?zhí)行之前 zval 已經(jīng)被銷毀了,這個(gè)字段就可能被移除了.next 在回收銷毀值的時(shí)候使用,這里不會(huì)深入.PHP應(yīng)用
修改動(dòng)機(jī)PHP應(yīng)用
下面說(shuō)說(shuō)關(guān)于內(nèi)存使用上的情況,這里說(shuō)的都是指在 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)存對(duì)齊),zval_gc_info 的大小是 32 個(gè)字節(jié).綜上,在堆(相對(duì)于棧)分配給 zval 的內(nèi)存必要額外的 16 個(gè)字節(jié),所以每個(gè) zval 在不同的地方一共必要用到 48 個(gè)字節(jié)(要理解上面的計(jì)算方式必要注意每個(gè)指針在 64 位的系統(tǒng)上也必要占用 8 個(gè)字節(jié)).PHP應(yīng)用
在這點(diǎn)上不管從什么方面去考慮都可以認(rèn)為 zval 的這種設(shè)計(jì)效率是很低的.比如 zval 在存儲(chǔ)整型的時(shí)候自己只需要 8 個(gè)字節(jié),即使考慮到需要存一些附加信息以及內(nèi)存對(duì)齊,額外 8 個(gè)字節(jié)應(yīng)該也是足夠的.PHP應(yīng)用
在存儲(chǔ)整型時(shí)原來(lái)確實(shí)需要 16 個(gè)字節(jié),但是實(shí)際上還有 16 個(gè)字節(jié)用于引用計(jì)數(shù)、16 個(gè)字節(jié)用于循環(huán)回收.所以說(shuō) zval 的內(nèi)存分配和釋放都是消耗很大的操作,我們有必要對(duì)其進(jìn)行優(yōu)化.PHP應(yīng)用
從這個(gè)角度思考:一個(gè)整型數(shù)據(jù)真的必要存儲(chǔ)引用計(jì)數(shù)、循環(huán)回收的信息并且單獨(dú)在堆上分配內(nèi)存嗎?答案是當(dāng)然不,這種處理方式一點(diǎn)都不好.PHP應(yīng)用
這里總結(jié)一下 PHP5 中 zval 實(shí)現(xiàn)方式存在的主要問(wèn)題:PHP應(yīng)用
zval 總是單獨(dú)從堆中分配內(nèi)存;PHP應(yīng)用
zval 總是存儲(chǔ)引用計(jì)數(shù)和循環(huán)回收的信息,即使是整型這種可能并不需要此類信息的數(shù)據(jù);
在使用對(duì)象或者資源時(shí),直接引用會(huì)導(dǎo)致兩次計(jì)數(shù)(原因會(huì)在下一部分講);
某些間接拜訪需要一個(gè)更好的處理方式.比如現(xiàn)在拜訪存儲(chǔ)在變量中的對(duì)象間接使用了四個(gè)指針(指針鏈的長(zhǎng)度為四).這個(gè)問(wèn)題也放到下一部分討論;
直接計(jì)數(shù)也就意味著數(shù)值只能在 zval 之間共享.如果想在 zval 和 hashtable key 之間共享一個(gè)字符串就不行(除非 hashtable key 也是 zval).PHP應(yīng)用
PHP7 中的 zvalPHP應(yīng)用
在 PHP7 中 zval 有了新的實(shí)現(xiàn)方式.最基礎(chǔ)的變化就是 zval 需要的內(nèi)存不再是單獨(dú)從堆上分配,不再本身存儲(chǔ)引用計(jì)數(shù).復(fù)雜數(shù)據(jù)類型(比如字符串、數(shù)組和對(duì)象)的引用計(jì)數(shù)由其自身來(lái)存儲(chǔ).這種實(shí)現(xiàn)方式有以下好處:PHP應(yīng)用
簡(jiǎn)單數(shù)據(jù)類型不需要單獨(dú)分配內(nèi)存,也不需要計(jì)數(shù);
不會(huì)再有兩次計(jì)數(shù)的情況.在對(duì)象中,只有對(duì)象自身存儲(chǔ)的計(jì)數(shù)是有效的;
由于現(xiàn)在計(jì)數(shù)由數(shù)值自身存儲(chǔ),所以也就可以和非 zval 結(jié)構(gòu)的數(shù)據(jù)共享,比如 zval 和 hashtable key 之間;
間接拜訪需要的指針數(shù)減少了.
PHP應(yīng)用
我們看看現(xiàn)在 zval 布局體的定義(現(xiàn)在在 zend_types.h 文件中):PHP應(yīng)用
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è)元素沒(méi)太大變化,仍然是一個(gè) value 聯(lián)合體.第二個(gè)成員是由一個(gè)表示類型信息的整型和一個(gè)包含四個(gè)字符變量的結(jié)構(gòu)體組成的聯(lián)合體(可以忽略 ZEND_ENDIAN_LOHI_4 宏,它只是用來(lái)辦理跨平臺(tái)大小端問(wèn)題的).這個(gè)子結(jié)構(gòu)中比較重要的部分是 type(和以前類似)和 type_flags,這個(gè)接下來(lái)會(huì)解釋.PHP應(yīng)用
上面這個(gè)地方也有一點(diǎn)小問(wèn)題:value 原來(lái)應(yīng)該占 8 個(gè)字節(jié),但是由于內(nèi)存對(duì)齊,哪怕只增加一個(gè)字節(jié),實(shí)際上也是占用 16 個(gè)字節(jié)(使用一個(gè)字節(jié)就意味著需要額外的 8 個(gè)字節(jié)).但是顯然我們并不需要 8 個(gè)字節(jié)來(lái)存儲(chǔ)一個(gè) type 字段,所以我們?cè)?u1 的后面增加了了一個(gè)名為 u2 的聯(lián)合體.默認(rèn)情況下是用不到的,需要使用的時(shí)候可以用來(lái)存儲(chǔ) 4 個(gè)字節(jié)的數(shù)據(jù).這個(gè)聯(lián)合體可以滿足不同場(chǎng)景下的需求.PHP應(yīng)用
PHP7 中 value 的布局定義如下:PHP應(yīng)用
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.它只會(huì)直接存儲(chǔ)整型(lval)或者浮點(diǎn)型(dval)數(shù)據(jù),其他情況下都是指針(上面提到過(guò),指針占用 8 個(gè)字節(jié),最下面的結(jié)構(gòu)體由兩個(gè) 4 字節(jié)的無(wú)符號(hào)整型組成).上面所有的指針類型(除了特殊標(biāo)記的)都有一個(gè)同樣的頭(zend_refcounted)用來(lái)存儲(chǔ)引用計(jì)數(shù):PHP應(yīng)用
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)體肯定會(huì)包括一個(gè)存儲(chǔ)引用計(jì)數(shù)的字段.除此之外還有 type、flags 和 gc_info.type 存儲(chǔ)的和 zval 中的 type 相同的內(nèi)容,這樣 GC 在不存儲(chǔ) zval 的情況下單獨(dú)使用引用計(jì)數(shù).flags 在不同的數(shù)據(jù)類型中有不同的用途,這個(gè)放到下一部分講.PHP應(yīng)用
gc_info 和 PHP5 中的 buffered 作用相同,不過(guò)不再是位于根緩沖區(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).PHP應(yīng)用
zval 內(nèi)存管理PHP應(yīng)用
上文提到過(guò) zval 需要的內(nèi)存不再單獨(dú)從堆上分配.但是顯然總要有地方來(lái)存儲(chǔ)它,所以會(huì)存在哪里呢?實(shí)際上大多時(shí)候它還是位于堆中(所以前文中提到的地方重點(diǎn)不是堆,而是單獨(dú)分配),只不過(guò)是嵌入到其他的數(shù)據(jù)結(jié)構(gòu)中的,好比 hashtable 和 bucket 現(xiàn)在就會(huì)直接有一個(gè) zval 字段而不是指針.所以函數(shù)表編譯變量和對(duì)象屬性在存儲(chǔ)時(shí)會(huì)是一個(gè) zval 數(shù)組并得到一整塊內(nèi)存而不是散落在各處的 zval 指針.之前的 zval * 現(xiàn)在都變成了 zval.PHP應(yīng)用
之前當(dāng) zval 在一個(gè)新的地方使用時(shí)會(huì)復(fù)制一份 zval * 并增加一次引用計(jì)數(shù).現(xiàn)在就直接復(fù)制 zval 的值(忽略 u2),某些情況下可能會(huì)增加其布局指針指向的引用計(jì)數(shù)(如果在進(jìn)行計(jì)數(shù)).PHP應(yīng)用
那么 PHP 怎么知道 zval 是否正在計(jì)數(shù)呢?不是所有的數(shù)據(jù)類型都能知道,因?yàn)橛行╊愋?好比字符串或數(shù)組)并不是總需要進(jìn)行引用計(jì)數(shù).所以 type_info 字段就是用來(lái)記錄 zval 是否在進(jìn)行計(jì)數(shù)的,這個(gè)字段的值有以下幾種情況:PHP應(yīng)用
#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 類型.PHP應(yīng)用
type_info 的三個(gè)主要的屬性就是『可計(jì)數(shù)』(refcounted)、『可回收』(collectable)和『可復(fù)制』(copyable).計(jì)數(shù)的問(wèn)題上面已經(jīng)提過(guò)了.『可回收』用于標(biāo)記 zval 是否參與循環(huán),不如字符串通常是可計(jì)數(shù)的,但是你卻沒(méi)方法給字符串制造一個(gè)循環(huán)引用的情況.PHP應(yīng)用
是否可復(fù)制用于表示在復(fù)制時(shí)是否需要在復(fù)制時(shí)制造(原文用的 "duplication" 來(lái)表述,用中文表達(dá)出來(lái)可能不是很好理解)一份一模一樣的實(shí)體."duplication" 屬于深度復(fù)制,好比在復(fù)制數(shù)組時(shí),不僅僅是簡(jiǎn)單增加數(shù)組的引用計(jì)數(shù),而是制造一份全新值一樣的數(shù)組.但是某些類型(好比對(duì)象和資源)即使 "duplication" 也只能是增加引用計(jì)數(shù),這種就屬于不可復(fù)制的類型.這也和對(duì)象和資源現(xiàn)有的語(yǔ)義匹配(現(xiàn)有,PHP7 也是這樣,不單是 PHP5).PHP應(yīng)用
下面的表格上標(biāo)明了不同的類型會(huì)使用哪些標(biāo)志(x 標(biāo)志的都是有的特性).『簡(jiǎn)單類型』(simple types)指的是整型或布爾類型這些不使用指針指向一個(gè)結(jié)構(gòu)體的類型.下表中也有『不可變』(immutable)的標(biāo)志,它用來(lái)標(biāo)志不可變數(shù)組的,這個(gè)在下一部分再詳述.PHP應(yīng)用
interned string(保存字符)在這之前沒(méi)有提過(guò),其實(shí)就是函數(shù)名、變量名等無(wú)需計(jì)數(shù)、不可重復(fù)的字符串.PHP應(yīng)用
??????????????? | 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???? |???????????? |????????? |PHP應(yīng)用
要理解這一點(diǎn),我們可以來(lái)看幾個(gè)例子,這樣可以更好的認(rèn)識(shí) zval 內(nèi)存管理是怎么工作的.PHP應(yīng)用
下面是整數(shù)行為模式,在上文中 PHP5 的例子的基礎(chǔ)上進(jìn)行了一些簡(jiǎn)化 :PHP應(yīng)用
<?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)
這個(gè)過(guò)程其實(shí)挺簡(jiǎn)單的.現(xiàn)在整數(shù)不再是共享的,變量直接就會(huì)分離成兩個(gè)單獨(dú)的 zval,由于現(xiàn)在 zval 是內(nèi)嵌的所以也不必要單獨(dú)分配內(nèi)存,所以這里的注釋中使用 = 來(lái)表示的而不是指針?lè)?hào) ->,unset 時(shí)變量會(huì)被標(biāo)記為 IS_UNDEF.下面看一下更復(fù)雜的情況:PHP應(yīng)用
<?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 分離在這里進(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 的布局體.修改其中一個(gè)數(shù)組的值時(shí)才會(huì)進(jìn)行復(fù)制.這點(diǎn)和 PHP5 的情況類似.PHP應(yīng)用
類型(Types)PHP應(yīng)用
我們大概看一下 PHP7 支持哪些類型(zval 使用的類型標(biāo)志):PHP應(yīng)用
/* 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):PHP應(yīng)用
IS_UNDEF 用來(lái)標(biāo)記之前為 NULL 的 zval 指針(和 IS_NULL 并不沖突).好比在上面的例子中使用 unset 注銷變量;
IS_BOOL 現(xiàn)在分割成了 IS_FALSE 和 IS_TRUE 兩項(xiàng).現(xiàn)在布爾類型的標(biāo)記是直接記錄到 type 中,這么做可以優(yōu)化類型檢查.不過(guò)這個(gè)變化對(duì)用戶是透明的,還是只有一個(gè)『布爾』類型的數(shù)據(jù)(PHP 腳本中).PHP應(yīng)用
PHP 引用不再使用 is_ref 來(lái)標(biāo)志,而是使用 IS_REFERENCE 類型.這個(gè)也要放到下一部分講;
IS_INDIRECT? 和? IS_PTR 是特殊的內(nèi)部標(biāo)志.PHP應(yīng)用
實(shí)際上上面的列表中應(yīng)該還存在兩個(gè) fake types,這里忽略了.PHP應(yīng)用
IS_LONG 類型表現(xiàn)的是一個(gè) zend_long 的值,而不是原生的 C 語(yǔ)言的 long 類型.原因是 Windows 的 64 位系統(tǒng)(LLP64)上的 long 類型只有 32 位的位深度.所以 PHP5 在 Windows 上只能使用 32 位的數(shù)字.PHP7 允許你在 64 位的操作系統(tǒng)上使用 64 位的數(shù)字,即使是在 Windows 上面也可以.PHP應(yīng)用
zend_refcounted 的內(nèi)容會(huì)在下一部門講.下面看看 PHP 引用的實(shí)現(xiàn).PHP應(yīng)用
引用PHP應(yīng)用
PHP7 使用了和 PHP5 中完全不同的辦法來(lái)處理 PHP & 符號(hào)引用的問(wèn)題(這個(gè)改動(dòng)也是 PHP7 開(kāi)發(fā)過(guò)程中大量 bug 的根源).我們先從 PHP5 中 PHP 引用的實(shí)現(xiàn)方式說(shuō)起.PHP應(yīng)用
通常情況下, 寫時(shí)復(fù)制原則意味著當(dāng)你修改一個(gè) zval 之前必要對(duì)其進(jìn)行分離來(lái)保證始終修改的只是某一個(gè) PHP 變量的值.這就是傳值調(diào)用的含義.PHP應(yīng)用
但是使用 PHP 引用時(shí)這條規(guī)則就不適用了.如果一個(gè) PHP 變量是 PHP 引用,就意味著你想要在將多個(gè) PHP 變量指向同一個(gè)值.PHP5 中的 is_ref 標(biāo)記就是用來(lái)注明一個(gè) PHP 變量是不是 PHP 引用,在修改時(shí)需不需要進(jìn)行分離的.好比:PHP應(yīng)用
<?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]) // 因?yàn)?is_ref 的值是 1, 所以 PHP 不會(huì)對(duì) zval 進(jìn)行分離
但是這個(gè)設(shè)計(jì)的一個(gè)很大的問(wèn)題在于它無(wú)法在一個(gè) PHP 引用變量和 PHP 非引用變量之間共享同一個(gè)值.好比下面這種情況:PHP應(yīng)用
<?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 還是需要進(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 的語(yǔ)句就不會(huì)修改 $a 和 $b 的值.
這種行為方式也導(dǎo)致在 PHP 中使用引用比普通的值要慢.好比下面這個(gè)例子:PHP應(yīng)用
<?php $array = range(0, 1000000); $ref =& $array; var_dump(count($array)); // <-- 這里會(huì)進(jìn)行分離
因?yàn)?count() 只接受傳值調(diào)用,但是 $array 是一個(gè) PHP 引用,所以 count() 在執(zhí)行之前實(shí)際上會(huì)有一個(gè)對(duì)數(shù)組進(jìn)行完整的復(fù)制的過(guò)程.如果 $array 不是引用,這種情況就不會(huì)產(chǎn)生了.PHP應(yīng)用
現(xiàn)在我們來(lái)看看 PHP7 中 PHP 引用的實(shí)現(xiàn).因?yàn)?zval 不再單獨(dú)分配內(nèi)存,也就沒(méi)方法再使用和 PHP5 中相同的實(shí)現(xiàn)了.所以增加了一個(gè) IS_REFERENCE 類型,并且專門使用 zend_reference 來(lái)存儲(chǔ)引用值:PHP應(yīng)用
struct _zend_reference { zend_refcounted gc; zval val; };
本色上 zend_reference 只是增加了引用計(jì)數(shù)的 zval.所有引用變量都會(huì)存儲(chǔ)一個(gè) zval 指針并且被標(biāo)記為 IS_REFERENCE.val 和其他的 zval 的行為一樣,尤其是它也可以在共享其所存儲(chǔ)的復(fù)雜變量的指針,比如數(shù)組可以在引用變量和值變量之間共享.PHP應(yīng)用
我們還是看例子,這次是 PHP7 中的語(yǔ)義.為了簡(jiǎn)潔明了這里不再單獨(dú)寫出 zval,只展示它們指向的布局體:PHP應(yīng)用
<?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])
上面的例子中進(jìn)行引用傳遞時(shí)會(huì)創(chuàng)建一個(gè) zend_reference,注意它的引用計(jì)數(shù)是 2(因?yàn)橛袃蓚€(gè)變量在使用這個(gè) PHP 引用).但是值自己的引用計(jì)數(shù)是 1(因?yàn)?zend_reference 只是有一個(gè)指針指向它).下面看看引用和非引用混合的情況:PHP應(yīng)用
<?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) ---^ // 注意所有變量共享同一個(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í)候才會(huì)對(duì) zend_array 進(jìn)行賦值
這里和 PHP5 最大的不同就是所有的變量都可以共享同一個(gè)數(shù)組,即使有的是 PHP 引用有的不是.只有當(dāng)其中某一部分被修改的時(shí)候才會(huì)對(duì)數(shù)組進(jìn)行分離.這也意味著使用 count() 時(shí)即使給其傳遞一個(gè)很大的引用數(shù)組也是平安的,不會(huì)再進(jìn)行復(fù)制.不過(guò)引用仍然會(huì)比普通的數(shù)值慢,因?yàn)榇嬖谛枰獮?zend_reference 結(jié)構(gòu)體分配內(nèi)存(間接)并且引擎本身處理這一塊兒也不快的的原因.PHP應(yīng)用
結(jié)語(yǔ)PHP應(yīng)用
總結(jié)一下 PHP7 中最重要的改變就是 zval 不再單獨(dú)從堆上分配內(nèi)存并且不本身存儲(chǔ)引用計(jì)數(shù).需要使用 zval 指針的復(fù)雜類型(比如字符串、數(shù)組和對(duì)象)會(huì)本身存儲(chǔ)引用計(jì)數(shù).這樣就可以有更少的內(nèi)存分配操作、更少的間接指針使用以及更少的內(nèi)存分配.PHP應(yīng)用
在下篇文章給大家介紹變量在 PHP7 內(nèi)部的實(shí)現(xiàn)(二),感興趣的朋友繼續(xù)存眷.PHP應(yīng)用
《PHP學(xué)習(xí):變量在 PHP7 內(nèi)部的實(shí)現(xiàn)(一)》是否對(duì)您有啟發(fā),歡迎查看更多與《PHP學(xué)習(xí):變量在 PHP7 內(nèi)部的實(shí)現(xiàn)(一)》相關(guān)教程,學(xué)精學(xué)透。維易PHP學(xué)院為您提供精彩教程。
轉(zhuǎn)載請(qǐng)注明本頁(yè)網(wǎng)址:
http://www.fzlkiss.com/jiaocheng/8195.html