迄今爲止,咱們向HashTables
中加入的zval
要麼是新建的,要麼是剛拷貝的。它們都是獨立的,只佔用本身的資源且只存在於某個HashTable中。做爲一個語言設計的概念,建立和拷貝變量的方法是「很好」的,可是習慣了C程序設計就會知道,經過避免拷貝大塊的數據(除非絕對必須)來節約內存和CPU時間並很多見。考慮這段用戶代碼:php
<?php $a = file_get_contents('fourMegabyteLogFile.log'); $b = $a; unset($a);
若是執行zval_copy_ctor()
(將會對字符串內容執行estrndup())將$a拷貝給$b,那麼這個簡短的腳本實際會用掉8M內存來存儲同一4M文件的兩份相同的副本。在最後一步取消$a只會更糟,由於原始字符串被efree()了。用C作這個將會很簡單,大概是這樣:b = a; a = NULL;。數組
Zend引擎的作法更聰明。當建立$a時,會建立一個潛在的string類型的zval,它含有日至文件的內容。這個zval經過調用zend_hash_add()被
賦給$a變量。當$a被拷貝給$b,引擎作相似下面的事情:php7
{ zval **value; zend_hash_find(EG(active_symbol_table), "a", sizeof("a"), (void**)&value); ZVAL_ADDREF(*value); zend_hash_add(EG(active_symbol_table), "b", sizeof("b"), value,sizeof(zval*)); }
實際代碼會更復雜點,簡單的說,就是經過引用計數來記錄zval在符號表中、數組中、或其餘地方被引用的次數。這樣$b = $a
賦值只要將其引用計數+1,而不用去進行內容拷貝。ui
當用戶空間代碼調用unset($a)
,引擎對該變量執行zval_ptr_dtor()
。在前面用到的zval_ptr_dtor()
中,你看不到的事實是,這個調用沒有必要銷燬該zval和它的內容。實際工做是減小refcount。若是,且僅僅是若是,引用計數變成了0,Zend引擎會銷燬該zval。設計
有些簡單數據類型不須要單獨分配內存,也不須要計數;PHP7中zval的long和double類型是 不須要
引用計數的。指針
php7的zval結構從新定義了,都有一個一樣的頭(zend_refcounted)用來存儲引用計數:code
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;
有兩種方法引用zval。第一種,如上文示範的,被稱爲寫複製引用(copy-on-write referencing)。第二種形式是徹底引用(full referencing);當提及「引用」時,用戶空間代碼的編寫者更熟悉這種, 以用戶空間代碼的形式出現相似於:$a = &$b;
。對象
在zval中,這兩種類型的區別在於它的is_ref成員的值,0表示寫複製引用,非0表示徹底引用。注意,一個zval不可能同時具備兩種引用類型。因此,若是變量起初是is_ref(即徹底引用-譯註),而後以拷貝的方式賦給新的變量,那麼必將執行一個徹底拷貝。考慮下面的用戶空間代碼:內存
<?php $a = []; //$a -> zend_array_1(refcount=1, value=[]) $b = &$a; //$a, $b -> zend_reference_1(refcount=2) -> zend_array_1(refcount=1, value=[]) $c = $a; //// $a, $b, $c -> zend_array_1(refcount=3, value=[])
在這段代碼中,爲$a建立並初始化了一個zval,將is_ref設爲0,將refcount設爲1。當$a被$b引用時,is_ref變爲1,refcount遞增至2。當拷貝至$c時,Zend引擎不能只是遞增refcount至3,由於如此則$c變成了$a的徹底引用。關閉is_ref也不行,由於如此會使$b看起來像是$a的一份拷貝而不是引用。因此此時分配了一個新的zval,並使用zval_copy_ctor()把原始(zval)的值拷貝給它。原始zval仍爲is_ref==一、refcount==2,同時新zval則爲is_ref=0、refcount=1。如今來看另外一塊內容相同的代碼塊,只是順序稍有不一樣:ci
<?php $a = []; //$a -> zend_array_1(refcount=1, value=[]) $c = $a; // $a, $c -> zval_1(type=IS_ARRAY, refcount=2, is_ref=0) -> HashTable_1(value=[]) $b = &$a; // $c -> zval_1(type=IS_ARRAY, refcount=2, is_ref=0) -> HashTable_1(value=[]) // $b, $a -> zval_1(type=IS_ARRAY, refcount=2, is_ref=1) -> HashTable_2(value=[]) // $b 是 $a 的引用, 但卻不是 $a 的 $c, 因此這裏 zval 仍是須要進行復制 // 這樣咱們就有了兩個 zval, 一個 is_ref 的值是 0, 一個 is_ref 的值是 1.
全部的變量均可以共享同一個數組,最終結果不變,$b是$a的徹底引用,而且$c是$a的一份拷貝。然而此次的內部效果稍有區別。如前,開始時爲$a建立一個is_ref==0而且refcount=1的新zval。$c = $a;語句將同一個zval賦給$c變量,同時將refcount增至2,is_ref還是0。當Zend引擎遇到$b = &$a;,它想要只是將is_ref設爲1,可是固然不行,由於那將影響到$c。因此改成建立新的zval並用zval_copy_ctor()將原始(zval)的內容拷貝給它。而後遞減原始zval的refcount以代表$a再也不使用該zval。代替地,(Zend)設置新zval的is_ref爲一、refcount爲2,而且更新$a和$b變量指向它(新zval)。
<?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,因爲如今 zval 是內嵌的因此也不須要單獨分配內存,因此這裏的註釋中使用 = 來表示的而不是指針符號 ->,unset 時變量會被標記爲 IS_UNDEF。
PHP7 中最重要的改變就是 zval 再也不單獨從堆上分配內存而且不本身存儲引用計數。須要使用 zval 指針的複雜類型(好比字符串、數組和對象)會本身存儲引用計數。這樣就能夠有更少的內存分配操做、更少的間接指針使用以及更少的內存分配。