一、PHP7中的zval使用棧記憶體
在 PHP7 中 zval 有了新的實作方式。最基礎的變化就是 zval 需要的記憶體不再是單獨從堆上配置設定,不再自己存儲引用計數。複雜資料類型(比如字元串、數組和對象)的引用計數由其自身來存儲。這種實作方式有以下好處:
- 簡單資料類型不需要單獨配置設定記憶體,也不需要計數;
- 不會再有兩次計數的情況。在對象中,隻有對象自身存儲的計數是有效的;
- 由于現在計數由數值自身存儲,是以也就可以和非 zval 結構的資料共享,比如 zval 和 hashtable key 之間;
- 間接通路需要的指針數減少了。
上文提到過 zval 需要的記憶體不再單獨從堆上配置設定。但是顯然總要有地方來存儲它,是以會存在哪裡呢?實際上大多時候它還是位于堆中(是以前文中提到的地方重點不是
堆
,而是
單獨配置設定
),隻不過是嵌入到其他的資料結構中的,比如 hashtable 和 bucket 現在就會直接有一個 zval 字段而不是指針。php7直接使用棧記憶體,好處是少了一次記憶體配置設定。php程式中回大量建立變量,是以php7會在棧上預配置設定一塊記憶體來存放這些zval,來節省大量的記憶體配置設定和管理操作。
php5
zval *val ; MAKE_STD_ZVAL(val)
php7
zval val;
這個新的zval在64位環境下,現在隻需要16個位元組(2個指針size), 它主要分為倆個部分,
value
和擴充字段, 而擴充字段又分為
u1
和
u2
倆個部分, 其中
u1
是type info,
u2
是各種輔助字段.
其中
value
部分, 是一個
size_t
大小(一個指針大小), 可以儲存一個指針, 或者一個
long
, 或者一個
double
.
而type info部分則儲存了這個zval的類型. 擴充輔助字段則會在多個其他地方使用, 比如
next
, 就用在取代Hashtable中原來的拉鍊指針, 這部分會在以後介紹HashTable的時候再來詳解.
結構體定義在
Zend/zend_types.h
中,定義内容如下所示:
struct _zval_struct {
zend_value value; /* value */
union {
struct {
ZEND_ENDIAN_LOHI_3(
zend_uchar type, /* active type */
zend_uchar type_flags,
union {
uint16_t extra; /* not further specified */
} u)
} v;
uint32_t type_info;
} u1;
union {
uint32_t next; /* hash collision chain */
uint32_t cache_slot; /* cache slot (for RECV_INIT) */
uint32_t opline_num; /* opline number (for FAST_CALL) */
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 */
uint32_t access_flags; /* class constant access flags */
uint32_t property_guard; /* single property guard */
uint32_t constant_flags; /* constant flags */
uint32_t extra; /* not further specified */
} u2;
};
zend_value
結構體的第一個變量是zend_value,用于存放變量的值,比如整型、浮點型、引用計數、字元串、數組、對象、資源等。
zend_value
定義了衆多類型的指針,但這些類型并不都是變量的類型,有些是給核心自己使用的,比如指針ast、zv、ptr。
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;
u1
u1是一個聯合體,它聯合了結構體
v
和整型
type_info
。下面我們先來看一下結構體
v
的構成。
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;
type
type是指變量的類型,剛在
2.1
中講到了zend_value是用來存儲變量的值,是以也應該有地方存儲變量的類型,而這就是type的職責。以下是PHP定義的所有變量類型,有我們熟知的布爾、NULL、浮點、數組、字元串等類型。也有陌生的undef、indirect、ptr類型
其中PHP5的時候的
IS_BOOL
類型, 現在拆分成了
IS_FALSE
和
IS_TRUE
倆種類型. 而原來的引用是一個标志位, 現在的引用是一種新的類型.
對于
IS_INDIRECT
和
IS_PTR
來說, 這倆個類型是用在内部的保留類型, 使用者不會感覺到
。
/* 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
/* fake types */
#define _IS_BOOL 13
#define IS_CALLABLE 14
/* internal types */
#define IS_INDIRECT 15
#define IS_PTR 17
從PHP7開始, 對于在zval的
value
字段中能儲存下的值, 就不再對他們進行引用計數了, 而是在拷貝的時候直接指派, 這樣就省掉了大量的引用計數相關的操作, 這部分類型有:
IS_LONG
IS_DOUBLE
當然對于那種根本沒有值, 隻有類型的類型, 也不需要引用計數了:
IS_NULL
IS_FALSE
IS_TRUE
而對于複雜類型, 一個
size_t
儲存不下的, 那麼我們就用
value
來儲存一個指針, 這個指針指向這個具體的值, 引用計數也随之作用于這個值上, 而不在是作用于zval上了:
IS_STRING
IS_ARRAY
IS_OBJECT
IS_RESOURCE
...
以
IS_ARRAY
為例:
struct _zend_array {
zend_refcounted_h gc;
union {
struct {
ZEND_ENDIAN_LOHI_4(
zend_uchar flags,
zend_uchar nApplyCount,
zend_uchar nIteratorsCount,
zend_uchar reserve)
} v;
uint32_t flags;
} u;
uint32_t nTableMask;
Bucket *arData;
uint32_t nNumUsed;
uint32_t nNumOfElements;
uint32_t nTableSize;
uint32_t nInternalPointer;
zend_long nNextFreeElement;
dtor_func_t pDestructor;
};
zval.value.arr
将指向上面的這樣的一個結構體, 由它實際儲存一個數組, 引用計數部分儲存在
zend_refcounted_h
結構中:
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;
所有的複雜類型的定義, 開始的時候都是
zend_refcounted_h
結構, 這個結構裡除了引用計數以外, 還有GC相關的結構. 進而在做GC回收的時候, GC不需要關心具體類型是什麼, 所有的它都可以當做
zend_refcounted*
結構來處理.
gc_info
和 PHP5 中的
buffered
作用相同,不過不再是位于根緩沖區的指針,而是一個索引數字。因為以前根緩沖區的大小是固定的(10000 個元素),是以使用一個 16 位(2 位元組)的數字代替 64 位(8 位元組)的指針足夠了。
gc_info
中同樣包含一個『顔色』位用于回收時标記結點。
type_flags
可以把它了解為子類型,上面提到了變量的類型,這個是針對不同類型的子類型或标記,
type_flags
一共有以下6種。
/* zval.u1.v.type_flags */
#define IS_TYPE_CONSTANT (1<<0) /* 常量 */
#define IS_TYPE_IMMUTABLE (1<<1) /* 不可變的類型, 比如存在共享記憶體的數組 */
#define IS_TYPE_REFCOUNTED (1<<2) /* 需要引用計數的類型 */
#define IS_TYPE_COLLECTABLE (1<<3) /* 可能包含循環引用的類型(IS_ARRAY, IS_OBJECT) */
#define IS_TYPE_COPYABLE (1<<4) /* 可被複制的類型, 還記得我之前講的對象和資源的例外麼? 對象和資源就不是 */
#define IS_TYPE_SYMBOLTABLE (1<<5) /* zval儲存的是全局符号表, 這個以後沒用了, 但還保留着相容,下個版本會去掉 */
作用于字元串的有:
-
IS_STR_PERSISTENT //是malloc配置設定記憶體的字元串
-
IS_STR_INTERNED //INTERNED STRING
-
IS_STR_PERMANENT //不可變的字元串, 用作哨兵作用
-
IS_STR_CONSTANT //代表常量的字元串
-
IS_STR_CONSTANT_UNQUALIFIED //帶有可能命名空間的常量字元串
作用于數組的有:
#define IS_ARRAY_IMMUTABLE //同IS_TYPE_IMMUTABLE
作用于對象的有:
-
IS_OBJ_APPLY_COUNT //遞歸保護
-
IS_OBJ_DESTRUCTOR_CALLED //析構函數已經調用
-
IS_OBJ_FREE_CALLED //清理函數已經調用
-
IS_OBJ_USE_GUARDS //魔術方法遞歸保護
-
IS_OBJ_HAS_GUARDS //是否有魔術方法遞歸保護标志
除了資料類型以外, 以前的經驗也告訴我們, 一個資料除了它的類型以外, 還應該有很多其他的屬性, 比如對于INTERNED STRING,它是一種在整個PHP請求期都存在的字元串(比如你寫在代碼中的字面量), 它不會被引用計數回收. 在5.4的版本中我們是通過預先申請一塊記憶體, 然後再這個記憶體中配置設定字元串, 最後用指針位址來比較, 如果一個字元串是屬于INTERNED STRING的記憶體範圍内, 就認為它是INTERNED STRING. 這樣做的缺點顯而易見, 就是當記憶體不夠的時候, 我們就沒有辦法配置設定INTERNED STRING了, 另外也非常醜陋, 是以如果一個字元串能有一些屬性定義則這個實作就可以變得很優雅.
還有, 比如現在我們對于
IS_LONG
,
IS_TRUE
等類型不再進行引用計數了, 那麼當我們拿到一個zval的時候如何判斷它需要不需要引用計數呢? 想當然的我們可能會說用:
if (Z_TYPE_P(zv) >= IS_STRING) {
//需要引用計數
}
但是你忘了, 還有INTERNED STRING的存在啊, 是以你也許要這麼寫了:
if (Z_TYPE_P(zv) >= IS_STRING && !IS_INTERNED(Z_STR_P(zv))) {
//需要引用計數
}
是不是已經讓你感覺到有點不對勁了? 嗯,别急, 還有呢, 我們還在5.6的時候引入了常量數組, 這個數組呢會存儲在Opcache的共享記憶體中, 它也不需要引用計數:
if (Z_TYPE_P(zv) >= IS_STRING && !IS_INTERNED(Z_STR_P(zv))
&& (Z_TYPE_P(zv) != IS_ARRAY || !Z_IS_IMMUTABLE(Z_ARRVAL(zv)))) {
//需要引用計數
}
你是不是也覺得這簡直太醜陋了, 簡直不能忍受這樣墨迹的代碼, 對吧?
是的,我們早想到了,回頭看之前的zval定義, 注意到
type_flags
了麼? 我們引入了一個标志位, 叫做
IS_TYPE_REFCOUNTED
, 它會儲存在
zval.u1.v.type_flags
中, 我們對于需要引用計數的類型就賦予這個标志, 是以上面的判斷就可以變得很優雅:
if (!(Z_TYPE_FLAGS(zv) & IS_TYPE_REFCOUNTED)) {
}
而對于INTERNED STRING來說, 這個
IS_STR_INTERNED
标志位應該是作用于字元串本身而不是zval的.
const_flags
常量類型的标記,對應的屬性為:
/* zval.u1.v.const_flags */
#define IS_CONSTANT_UNQUALIFIED 0x010
#define IS_LEXICAL_VAR 0x020
#define IS_LEXICAL_REF 0x040
#define IS_CONSTANT_CLASS 0x080 /* __CLASS__ in trait */
#define IS_CONSTANT_IN_NAMESPACE 0x100 /* used only in opline->extended_value */
type_info
type_info與結構體v共用記憶體,修改type_info等同于修改結構體v的值,是以type_info是v中四個char的組合。
u2
本來使用u1和zend_value就可以表示變量的,沒有必要定義u2,但是我們來看一下,如果沒有u2,在記憶體對齊的情況下zval記憶體大小為16個位元組,當聯合了u2後依然是占用16個位元組。既然有或沒有占用記憶體大小相同,不如用它來記錄一些附屬資訊。下面我們來看下u2都存儲了哪些内容。
2.3.1、next
用來解決哈希沖突問題,記錄沖突的下一個元素位置。
2.3.2、cache_slot
運作時緩存,在執行函數時回去緩存中查找,若緩存中沒有則到全局function表中查找。
2.3.3、lineno
檔案執行的行号,應用在AST節點上。Zend引擎在詞法和文法解析時會把目前執行的檔案行号記錄下來,記錄在zend_ast中的lineno中。
ZVAL預先配置設定
前面我們說過, PHP5的zval配置設定采用的是堆上配置設定記憶體, 也就是在PHP預案代碼中随處可見的MAKE_STD_ZVAL和ALLOC_ZVAL宏. 我們也知道了本來一個zval隻需要24個位元組, 但是算上gc_info, 其實配置設定了32個位元組, 再加上PHP自己的記憶體管理在配置設定記憶體的時候都會在記憶體前面保留一部分資訊,進而導緻實際上我們隻需要24位元組的記憶體, 但最後竟然配置設定48個位元組之多.
然而大部分的zval, 尤其是擴充函數内的zval, 我們想想它接受的參數來自外部的zval, 它把傳回值傳回給return_value, 這個也是來自外部的zval, 而中間變量的zval完全可以采用棧上配置設定. 也就是說大部分的内部函數都不需要在堆上配置設定記憶體, 它需要的zval都可以來自外部.
于是當時我們做了一個大膽的想法, 所有的zval都不需要單獨申請.
而這個也很容易證明, PHP腳本中使用的zval, 要麼存在于符号表, 要麼就以臨時變量(
IS_TMP_VAR
)或者編譯變量(
IS_CV
)的形式存在. 前者存在于一個Hashtable中, 而在PHP7中Hashtable預設儲存的就是zval, 這部分的zval完全可以在Hashtable配置設定的時候一次性配置設定出來, 後面的存在于execute_data之後, 數量也在編譯時刻确定好了, 也可以随着execute_data一次性配置設定, 是以我們确實不再需要單獨在堆上申請zval了.
是以, 在PHP7開始, 我們移除了MAKE_STD_ZVAL/ALLOC_ZVAL宏, 不再支援存堆記憶體上申請zval. 函數内部使用的zval要麼來自外面輸入, 要麼使用在棧上配置設定的臨時zval.
抽象的來說, 其實在PHP7中的zval, 已經變成了一個值指針, 它要麼儲存着原始值, 要麼儲存着指向一個儲存原始值的指針. 也就是說現在的zval相當于PHP5的時候的zval *. 隻不過相比于zval *, 直接存儲zval, 我們可以省掉一次指針解引用, 進而提高緩存友好性.
其實PHP7的性能, 我們并沒有引入什麼新的技術模式, 不過就是主要來自, 持續不懈的降低記憶體占用, 提高緩存友好性, 降低執行的指令數的這些原則而來的, 可以說PHP7的重構就是這三個原則.
PHP5 zval回顧
在PHP5的時候, zval的定義如下:
struct _zval_struct {
union {
long lval;
double dval;
struct {
char *val;
int len;
} str;
HashTable *ht;
zend_object_value obj;
zend_ast *ast;
} value;
zend_uint refcount__gc;
zend_uchar type;
zend_uchar is_ref__gc;
};
因為zval可以表示一切PHP中的資料類型, 是以它包含了一個type字段, 表示這個zval存儲的是什麼類型的值, 常見的可能選項是
IS_NULL
,
IS_LONG
,
IS_STRING
,
IS_ARRAY
,
IS_OBJECT
等等.
C 語言聯合體的特征是一次隻有一個成員是有效的并且配置設定的記憶體與需要記憶體最多的成員比對(也要考慮記憶體對齊)。所有成員都存儲在記憶體的同一個位置,根據需要存儲不同的值。根據type字段的值不同, 我們就要用不同的方式解讀value的值, 比如對于type是
IS_STRING
, 那麼我們應該用
value.str
來解讀
zval.value
字段, 而如果type是
IS_LONG
, 那麼我們就要用
value.lval
來解讀.
另外, 我們知道PHP是用引用計數來做基本的垃圾回收的, 是以zval中有一個
refcount__gc
字段, 表示這個zval的引用數目, 但這裡有一個要說明的, 在5.3以前, 這個字段的名字還叫做
refcount
, 5.3以後, 在引入新的垃圾回收算法來對付循環引用計數的時候, 作者加入了大量的宏來操作
refcount
, 為了能讓錯誤更快的顯現, 是以改名為
refcount__gc
, 迫使大家都使用宏來操作
refcount
.
類似的, 還有
is_ref
, 這個值表示了PHP中的一個類型是否是引用, 這裡我們可以看到是不是引用是一個标志位.
這就是PHP5時代的zval, 在2013年我們做PHP5的opcache JIT的時候, 因為JIT在實際項目中表現不佳, 我們轉而意識到這個結構體的很多問題. 而PHPNG項目就是從改寫這個結構體而開始的.
存在的問題
PHP5的zval定義是随着Zend Engine 2誕生的, 随着時間的推移, 當時設計的局限性也越來越明顯:
首先這個結構體的大小是(在64位系統)24個位元組, 我們仔細看這個
zval.value
聯合體, 其中
zend_object_value
是最大的長闆, 它導緻整個value需要16個位元組, 這個應該是很容易可以優化掉的, 比如把它挪出來, 用個指針代替,因為畢竟
IS_OBJECT
也不是最最常用的類型.
第二, 這個結構體的每一個字段都有明确的含義定義, 沒有預留任何的自定義字段, 導緻在PHP5時代做很多的優化的時候, 需要存儲一些和zval相關的資訊的時候, 不得不采用其他結構體映射, 或者外部包裝後打更新檔的方式來擴充zval, 比如5.3的時候新引入專門解決循環引用的GC, 它不得采用如下的比較hack的做法:
/* The following macroses override macroses from zend_alloc.h */
#undef ALLOC_ZVAL
#define ALLOC_ZVAL(z) \
do { \
(z) = (zval*)emalloc(sizeof(zval_gc_info)); \
GC_ZVAL_INIT(z); \
} while (0)
它用
zval_gc_info
劫持了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, 是以實際上來說我們在PHP5時代申請一個zval其實真正的是配置設定了32個位元組, 但其實GC隻需要關心
IS_ARRAY和IS_OBJECT
類型, 這樣就導緻了大量的記憶體浪費.
第三, PHP的zval大部分都是按值傳遞, 寫時拷貝的值, 但是有倆個例外, 就是對象和資源, 他們永遠都是按引用傳遞, 這樣就造成一個問題, 對象和資源在除了zval中的引用計數以外, 還需要一個全局的引用計數, 這樣才能保證記憶體可以回收. 是以在PHP5的時代, 以對象為例, 它有倆套引用計數, 一個是zval中的, 另外一個是obj自身的計數:
typedef struct _zend_object_store_bucket {
zend_bool destructor_called;
zend_bool valid;
union _store_bucket {
struct _store_object {
void *object;
zend_objects_store_dtor_t dtor;
zend_objects_free_object_storage_t free_storage;
zend_objects_store_clone_t clone;
const zend_object_handlers *handlers;
zend_uint refcount;
gc_root_buffer *buffered;
} obj;
struct {
int next;
} free_list;
} bucket;
} zend_object_store_bucket;
除了上面提到的兩套引用以外, 如果我們要擷取一個object, 則我們需要通過如下方式:
EG(objects_store).object_buckets[Z_OBJ_HANDLE_P(z)].bucket.obj
經過漫長的多次記憶體讀取, 才能擷取到真正的objec對象本身. 效率可想而知.
這一切都是因為Zend引擎最初設計的時候, 并沒有考慮到後來的對象.
第四, 我們知道PHP中, 大量的計算都是面向字元串的, 然而因為引用計數是作用在zval的, 那麼就會導緻如果要拷貝一個字元串類型的zval, 我們别無他法隻能複制這個字元串. 當我們把一個zval的字元串作為key添加到一個數組裡的時候, 我們别無他法隻能複制這個字元串. 雖然在PHP5.4的時候, 我們引入了INTERNED STRING, 但是還是不能根本解決這個問題.
還比如, PHP中大量的結構體都是基于Hashtable實作的, 增删改查Hashtable的操作占據了大量的CPU時間, 而字元串要查找首先要求它的Hash值, 理論上我們完全可以把一個字元串的Hash值計算好以後, 就存下來, 避免再次計算等等
第五, 這個是關于引用的, PHP5的時代, 我們采用寫時分離, 但是結合到引用這裡就有了一個經典的性能問題:
<?php
function dummy($array) {}
$array = range(1, 100000);
$b = &$array;
dummy($array);
?>
當我們調用dummy的時候, 本來隻是簡單的一個傳值就行的地方, 但是因為$array曾經引用指派給了$b, 是以導緻$array變成了一個引用, 于是此處就會發生分離, 導緻數組複制, 進而極大的拖慢性能, 這裡有一個簡單的測試:
<?php
$array = range(1, 100000);
function dummy($array) {}
$i = 0;
$start = microtime(true);
while($i++ < 100) {
dummy($array);
}
printf("Used %sS\n", microtime(true) - $start);
$b = &$array; //注意這裡, 假設我不小心把這個Array引用給了一個變量
$i = 0;
$start = microtime(true);
while($i++ < 100) {
dummy($array);
}
printf("Used %sS\n", microtime(true) - $start);
?>
我們在5.6下運作這個例子, 得到如下結果:
$ php-5.6/sapi/cli/php /tmp/1.php
Used 0.00045204162597656S
Used 4.2051479816437S
相差1萬倍之多. 這就造成, 如果在一大段代碼中, 我不小心把一個變量變成了引用(比如foreach as &$v), 那麼就有可能觸發到這個問題, 造成嚴重的性能問題, 然而卻又很難排查.
第六, 也是最重要的一個, 為什麼說它重要呢? 因為這點促成了很大的性能提升, 我們習慣了在PHP5的時代調用
MAKE_STD_ZVAL
在堆記憶體上配置設定一個zval, 然後對他進行操作, 最後呢通過
RETURN_ZVAL
把這個zval的值"copy"給
return_value
, 然後又銷毀了這個zval, 比如
pathinfo
這個函數:
PHP_FUNCTION(pathinfo)
{
.....
MAKE_STD_ZVAL(tmp);
array_init(tmp);
.....
if (opt == PHP_PATHINFO_ALL) {
RETURN_ZVAL(tmp, 0, 1);
} else {
.....
}
這個tmp變量, 完全是一個臨時變量的作用, 我們又何必在堆記憶體配置設定它呢?
MAKE_STD_ZVAL/ALLOC_ZVAL
在PHP5的時候, 到處都有, 是一個非常常見的用法, 如果我們能把這個變量用棧配置設定, 那無論是記憶體配置設定, 還是緩存友好, 都是非常有利的
這裡總結一下 PHP5 中 zval 實作方式存在的主要問題:
- zval 總是單獨從堆中配置設定記憶體;
- zval 總是存儲引用計數和循環回收的資訊,即使是整型這種可能并不需要此類資訊的資料;
- 在使用對象或者資源時,直接引用會導緻兩次計數;
- 某些間接通路需要一個更好的處理方式。比如現在通路存儲在變量中的對象間接使用了四個指針(指針鍊的長度為四)。這個問題也放到下一部分讨論;
- 直接計數也就意味着數值隻能在 zval 之間共享。如果想在 zval 和 hashtable key 之間共享一個字元串就不行(除非 hashtable key 也是 zval)。
引用
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])
<?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 進行指派