這篇文章給大家分享的是有關(guān)PHP7內(nèi)核之zval是什么的內(nèi)容。小編覺得挺實(shí)用的,因此分享給大家做個(gè)參考。一起跟隨小編過來看看吧。
在輝南等地區(qū),都構(gòu)建了全面的區(qū)域性戰(zhàn)略布局,加強(qiáng)發(fā)展的系統(tǒng)性、市場前瞻性、產(chǎn)品創(chuàng)新能力,以專注、極致的服務(wù)理念,為客戶提供成都網(wǎng)站制作、網(wǎng)站設(shè)計(jì)、外貿(mào)網(wǎng)站建設(shè) 網(wǎng)站設(shè)計(jì)制作按需網(wǎng)站制作,公司網(wǎng)站建設(shè),企業(yè)網(wǎng)站建設(shè),品牌網(wǎng)站建設(shè),營銷型網(wǎng)站,成都外貿(mào)網(wǎng)站制作,輝南網(wǎng)站建設(shè)費(fèi)用合理。
zval回顧
在PHP5的時(shí)候, 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; };
對PHP5內(nèi)核有了解的同學(xué)應(yīng)該對這個(gè)結(jié)構(gòu)比較熟悉, 因?yàn)閦val可以表示一切PHP中的數(shù)據(jù)類型, 所以它包含了一個(gè)type字段, 表示這個(gè)zval存儲(chǔ)的是什么類型的值, 常見的可能選項(xiàng)是IS_NULL, IS_LONG, IS_STRING, IS_ARRAY, IS_OBJECT等等.
根據(jù)type字段的值不同, 我們就要用不同的方式解讀value的值, 這個(gè)value是個(gè)聯(lián)合體, 比如對于type是IS_STRING, 那么我們應(yīng)該用value.str來解讀zval.value字段, 而如果type是IS_LONG, 那么我們就要用value.lval來解讀.
另外, 我們知道PHP是用引用計(jì)數(shù)來做基本的垃圾回收的, 所以zval中有一個(gè)refcount__gc字段, 表示這個(gè)zval的引用數(shù)目, 但這里有一個(gè)要說明的, 在5.3以前, 這個(gè)字段的名字還叫做refcount, 5.3以后, 在引入新的垃圾回收算法來對付循環(huán)引用計(jì)數(shù)的時(shí)候, 作者加入了大量的宏來操作refcount, 為了能讓錯(cuò)誤更快的顯現(xiàn), 所以改名為refcount__gc, 迫使大家都使用宏來操作refcount.
類似的, 還有is_ref, 這個(gè)值表示了PHP中的一個(gè)類型是否是引用, 這里我們可以看到是不是引用是一個(gè)標(biāo)志位.
這就是PHP5時(shí)代的zval, 在2013年我們做PHP5的opcache JIT的時(shí)候, 因?yàn)镴IT在實(shí)際項(xiàng)目中表現(xiàn)不佳, 我們轉(zhuǎn)而意識(shí)到這個(gè)結(jié)構(gòu)體的很多問題. 而PHPNG項(xiàng)目就是從改寫這個(gè)結(jié)構(gòu)體而開始的.
存在的問題
PHP5的zval定義是隨著Zend Engine 2誕生的, 隨著時(shí)間的推移, 當(dāng)時(shí)設(shè)計(jì)的局限性也越來越明顯:
首先這個(gè)結(jié)構(gòu)體的大小是(在64位系統(tǒng))24個(gè)字節(jié), 我們仔細(xì)看這個(gè)zval.value聯(lián)合體, 其中zend_object_value是最大的長板, 它導(dǎo)致整個(gè)value需要16個(gè)字節(jié), 這個(gè)應(yīng)該是很容易可以優(yōu)化掉的, 比如把它挪出來, 用個(gè)指針代替,因?yàn)楫吘笽S_OBJECT也不是最最常用的類型.
第二, 這個(gè)結(jié)構(gòu)體的每一個(gè)字段都有明確的含義定義, 沒有預(yù)留任何的自定義字段, 導(dǎo)致在PHP5時(shí)代做很多的優(yōu)化的時(shí)候, 需要存儲(chǔ)一些和zval相關(guān)的信息的時(shí)候, 不得不采用其他結(jié)構(gòu)體映射, 或者外部包裝后打補(bǔ)丁的方式來擴(kuò)充zval, 比如5.3的時(shí)候新引入專門解決循環(huán)引用的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來擴(kuò)充了zval, 所以實(shí)際上來說我們在PHP5時(shí)代申請一個(gè)zval其實(shí)真正的是分配了32個(gè)字節(jié), 但其實(shí)GC只需要關(guān)心IS_ARRAY和IS_OBJECT類型, 這樣就導(dǎo)致了大量的內(nèi)存浪費(fèi).
還比如我之前做的Taint擴(kuò)展, 我需要對于給一些字符串存儲(chǔ)一些標(biāo)記, zval里沒有任何地方可以使用, 所以我不得不采用非常手段:
Z_STRVAL_PP(ppzval) = erealloc(Z_STRVAL_PP(ppzval), Z_STRLEN_PP(ppzval) + 1 + PHP_TAINT_MAGIC_LENGTH); PHP_TAINT_MARK(*ppzval, PHP_TAINT_MAGIC_POSSIBLE);
就是把字符串的長度擴(kuò)充一個(gè)int, 然后用magic number做標(biāo)記寫到后面去, 這樣的做法安全性和穩(wěn)定性在技術(shù)上都是沒有保障的
第三, PHP的zval大部分都是按值傳遞, 寫時(shí)拷貝的值, 但是有倆個(gè)例外, 就是對象和資源, 他們永遠(yuǎn)都是按引用傳遞, 這樣就造成一個(gè)問題, 對象和資源在除了zval中的引用計(jì)數(shù)以外, 還需要一個(gè)全局的引用計(jì)數(shù), 這樣才能保證內(nèi)存可以回收. 所以在PHP5的時(shí)代, 以對象為例, 它有倆套引用計(jì)數(shù), 一個(gè)是zval中的, 另外一個(gè)是obj自身的計(jì)數(shù):
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;
除了上面提到的兩套引用以外, 如果我們要獲取一個(gè)object, 則我們需要通過如下方式:
EG(objects_store).object_buckets[Z_OBJ_HANDLE_P(z)].bucket.obj
經(jīng)過漫長的多次內(nèi)存讀取, 才能獲取到真正的objec對象本身. 效率可想而知.
這一切都是因?yàn)閆end引擎最初設(shè)計(jì)的時(shí)候, 并沒有考慮到后來的對象. 一個(gè)良好的設(shè)計(jì), 一旦有了意外, 就會(huì)導(dǎo)致整個(gè)結(jié)構(gòu)變得復(fù)雜, 維護(hù)性降低, 這是一個(gè)很好的例子.
第四, 我們知道PHP中, 大量的計(jì)算都是面向字符串的, 然而因?yàn)橐糜?jì)數(shù)是作用在zval的, 那么就會(huì)導(dǎo)致如果要拷貝一個(gè)字符串類型的zval, 我們別無他法只能復(fù)制這個(gè)字符串. 當(dāng)我們把一個(gè)zval的字符串作為key添加到一個(gè)數(shù)組里的時(shí)候, 我們別無他法只能復(fù)制這個(gè)字符串. 雖然在PHP5.4的時(shí)候, 我們引入了INTERNED STRING, 但是還是不能根本解決這個(gè)問題.
還比如, PHP中大量的結(jié)構(gòu)體都是基于Hashtable實(shí)現(xiàn)的, 增刪改查Hashtable的操作占據(jù)了大量的CPU時(shí)間, 而字符串要查找首先要求它的Hash值, 理論上我們完全可以把一個(gè)字符串的Hash值計(jì)算好以后, 就存下來, 避免再次計(jì)算等等
第五, 這個(gè)是關(guān)于引用的, PHP5的時(shí)代, 我們采用寫時(shí)分離, 但是結(jié)合到引用這里就有了一個(gè)經(jīng)典的性能問題:
當(dāng)我們調(diào)用dummy的時(shí)候, 本來只是簡單的一個(gè)傳值就行的地方, 但是因?yàn)?array曾經(jīng)引用賦值給了$b, 所以導(dǎo)致$array變成了一個(gè)引用, 于是此處就會(huì)發(fā)生分離, 導(dǎo)致數(shù)組復(fù)制, 從而極大的拖慢性能, 這里有一個(gè)簡單的測試:
我們在5.6下運(yùn)行這個(gè)例子, 得到如下結(jié)果:
$ php-5.6/sapi/cli/php /tmp/1.php Used 0.00045204162597656s Used 4.2051479816437s
相差1萬倍之多. 這就造成, 如果在一大段代碼中, 我不小心把一個(gè)變量變成了引用(比如foreach as &$v), 那么就有可能觸發(fā)到這個(gè)問題, 造成嚴(yán)重的性能問題, 然而卻又很難排查.
第六, 也是最重要的一個(gè), 為什么說它重要呢? 因?yàn)檫@點(diǎn)促成了很大的性能提升, 我們習(xí)慣了在PHP5的時(shí)代調(diào)用MAKE_STD_ZVAL在堆內(nèi)存上分配一個(gè)zval, 然后對他進(jìn)行操作, 最后呢通過RETURN_ZVAL把這個(gè)zval的值”copy”給return_value, 然后又銷毀了這個(gè)zval, 比如pathinfo這個(gè)函數(shù):
PHP_FUNCTION(pathinfo) { ..... MAKE_STD_ZVAL(tmp); array_init(tmp); ..... if (opt == PHP_PATHINFO_ALL) { RETURN_ZVAL(tmp, 0, 1); } else { ..... }
這個(gè)tmp變量, 完全是一個(gè)臨時(shí)變量的作用, 我們又何必在堆內(nèi)存分配它呢? MAKE_STD_ZVAL/ALLOC_ZVAL在PHP5的時(shí)候, 到處都有, 是一個(gè)非常常見的用法, 如果我們能把這個(gè)變量用棧分配, 那無論是內(nèi)存分配, 還是緩存友好, 都是非常有利的
還有很多, 我就不一一詳細(xì)列舉了, 但是我相信你們也有了和我們當(dāng)時(shí)一樣的想法, zval必須得改改了, 對吧?
現(xiàn)在的zval
到了PHP7中, zval變成了如下的結(jié)構(gòu), 要說明的是, 這個(gè)是現(xiàn)在的結(jié)構(gòu), 已經(jīng)和PHPNG時(shí)候有了一些不同了, 因?yàn)槲覀冃略黾恿艘恍┙忉?(聯(lián)合體的字段), 但是總體大小, 結(jié)構(gòu), 是和PHPNG的時(shí)候一致的:
struct _zval_struct { union { 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; } 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; };
雖然看起來變得好大, 但其實(shí)你仔細(xì)看, 全部都是聯(lián)合體, 這個(gè)新的zval在64位環(huán)境下,現(xiàn)在只需要16個(gè)字節(jié)(2個(gè)指針size), 它主要分為倆個(gè)部分, value和擴(kuò)充字段, 而擴(kuò)充字段又分為u1和u2倆個(gè)部分, 其中u1是type info, u2是各種輔助字段.
其中value部分, 是一個(gè)size_t大小(一個(gè)指針大小), 可以保存一個(gè)指針, 或者一個(gè)long, 或者一個(gè)double.
而type info部分則保存了這個(gè)zval的類型. 擴(kuò)充輔助字段則會(huì)在多個(gè)其他地方使用, 比如next, 就用在取代Hashtable中原來的拉鏈指針, 這部分會(huì)在以后介紹HashTable的時(shí)候再來詳解.
類型
PHP7中的zval的類型做了比較大的調(diào)整, 總體來說有如下17種類型:
/* 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
其中PHP5的時(shí)候的IS_BOOL類型, 現(xiàn)在拆分成了IS_FALSE和IS_TRUE倆種類型. 而原來的引用是一個(gè)標(biāo)志位, 現(xiàn)在的引用是一種新的類型.
對于IS_INDIRECT和IS_PTR來說, 這倆個(gè)類型是用在內(nèi)部的保留類型, 用戶不會(huì)感知到, 這部分會(huì)在后續(xù)介紹HashTable的時(shí)候也一并介紹.
從PHP7開始, 對于在zval的value字段中能保存下的值, 就不再對他們進(jìn)行引用計(jì)數(shù)了, 而是在拷貝的時(shí)候直接賦值, 這樣就省掉了大量的引用計(jì)數(shù)相關(guān)的操作, 這部分類型有:
IS_LONG IS_DOUBLE
當(dāng)然對于那種根本沒有值, 只有類型的類型, 也不需要引用計(jì)數(shù)了:
IS_NULL IS_FALSE IS_TRUE
而對于復(fù)雜類型, 一個(gè)size_t保存不下的, 那么我們就用value來保存一個(gè)指針, 這個(gè)指針指向這個(gè)具體的值, 引用計(jì)數(shù)也隨之作用于這個(gè)值上, 而不在是作用于zval上了.
PHP7 zval示意圖
以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將指向上面的這樣的一個(gè)結(jié)構(gòu)體, 由它實(shí)際保存一個(gè)數(shù)組, 引用計(jì)數(shù)部分保存在zend_refcounted_h結(jié)構(gòu)中:
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;
所有的復(fù)雜類型的定義, 開始的時(shí)候都是zend_refcounted_h結(jié)構(gòu), 這個(gè)結(jié)構(gòu)里除了引用計(jì)數(shù)以外, 還有GC相關(guān)的結(jié)構(gòu). 從而在做GC回收的時(shí)候, GC不需要關(guān)心具體類型是什么, 所有的它都可以當(dāng)做zend_refcounted*結(jié)構(gòu)來處理.
另外有一個(gè)需要說明的就是大家可能會(huì)好奇的ZEND_ENDIAN_LOHI_4宏, 這個(gè)宏的作用是簡化賦值, 它會(huì)保證在大端或者小端的機(jī)器上, 它定義的字段都按照一樣順序排列存儲(chǔ), 從而我們在賦值的時(shí)候, 不需要對它的字段分別賦值, 而是可以統(tǒng)一賦值, 比如對于上面的array結(jié)構(gòu)為例, 就可以通過:
arr1.u.flags = arr2.u.flags;
一次完成相當(dāng)于如下的賦值序列:
arr1.u.v.flags = arr2.u.v.flags; arr1.u.v.nApplyCount = arr2.u.v.nApplyCount; arr1.u.v.nIteratorsCount = arr2.u.v.nIteratorsCount; arr1.u.v.reserve = arr2.u.v.reserve;
還有一個(gè)大家可能會(huì)問到的問題是, 為什么不把type類型放到zval類型的前面, 因?yàn)槲覀冎喇?dāng)我們?nèi)ビ靡粋€(gè)zval的時(shí)候, 首先第一點(diǎn)肯定是先去獲取它的類型. 這里的一個(gè)原因是, 一個(gè)是倆者差別不大, 另外就是考慮到如果以后JIT的話, zval的類型如果能夠通過類型推導(dǎo)獲得, 就根本沒有必要去讀取它的type值了.
標(biāo)志位
除了數(shù)據(jù)類型以外, 以前的經(jīng)驗(yàn)也告訴我們, 一個(gè)數(shù)據(jù)除了它的類型以外, 還應(yīng)該有很多其他的屬性, 比如對于INTERNED STRING,它是一種在整個(gè)PHP請求期都存在的字符串(比如你寫在代碼中的字面量), 它不會(huì)被引用計(jì)數(shù)回收. 在5.4的版本中我們是通過預(yù)先申請一塊內(nèi)存, 然后再這個(gè)內(nèi)存中分配字符串, 最后用指針地址來比較, 如果一個(gè)字符串是屬于INTERNED STRING的內(nèi)存范圍內(nèi), 就認(rèn)為它是INTERNED STRING. 這樣做的缺點(diǎn)顯而易見, 就是當(dāng)內(nèi)存不夠的時(shí)候, 我們就沒有辦法分配INTERNED STRING了, 另外也非常丑陋, 所以如果一個(gè)字符串能有一些屬性定義則這個(gè)實(shí)現(xiàn)就可以變得很優(yōu)雅.
還有, 比如現(xiàn)在我們對于IS_LONG, IS_TRUE等類型不再進(jìn)行引用計(jì)數(shù)了, 那么當(dāng)我們拿到一個(gè)zval的時(shí)候如何判斷它需要不需要引用計(jì)數(shù)呢? 想當(dāng)然的我們可能會(huì)說用:
if (Z_TYPE_P(zv) >= IS_STRING) { //需要引用計(jì)數(shù) }
但是你忘了, 還有INTERNED STRING的存在啊, 所以你也許要這么寫了:
if (Z_TYPE_P(zv) >= IS_STRING && !IS_INTERNED(Z_STR_P(zv))) { //需要引用計(jì)數(shù) }
是不是已經(jīng)讓你感覺到有點(diǎn)不對勁了? 嗯,別急, 還有呢, 我們還在5.6的時(shí)候引入了常量數(shù)組, 這個(gè)數(shù)組呢會(huì)存儲(chǔ)在Opcache的共享內(nèi)存中, 它也不需要引用計(jì)數(shù):
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)))) { //需要引用計(jì)數(shù) }
你是不是也覺得這簡直太丑陋了, 簡直不能忍受這樣墨跡的代碼, 對吧?
是的,我們早想到了,回頭看之前的zval定義, 注意到type_flags了么? 我們引入了一個(gè)標(biāo)志位, 叫做IS_TYPE_REFCOUNTED, 它會(huì)保存在zval.u1.v.type_flags中, 我們對于需要引用計(jì)數(shù)的類型就賦予這個(gè)標(biāo)志, 所以上面的判斷就可以變得很優(yōu)雅:
if (!(Z_TYPE_FLAGS(zv) & IS_TYPE_REFCOUNTED)) { }
而對于INTERNED STRING來說, 這個(gè)IS_STR_INTERNED標(biāo)志位應(yīng)該是作用于字符串本身而不是zval的.
那么類似這樣的標(biāo)志位一共有多少呢?作用于zval的有:
IS_TYPE_CONSTANT //是常量類型 IS_TYPE_IMMUTABLE //不可變的類型, 比如存在共享內(nèi)存的數(shù)組 IS_TYPE_REFCOUNTED //需要引用計(jì)數(shù)的類型 IS_TYPE_COLLECTABLE //可能包含循環(huán)引用的類型(IS_ARRAY, IS_OBJECT) IS_TYPE_COPYABLE //可被復(fù)制的類型, 還記得我之前講的對象和資源的例外么? 對象和資源就不是 IS_TYPE_SYMBOLTABLE //zval保存的是全局符號(hào)表, 這個(gè)在我之前做了一個(gè)調(diào)整以后沒用了, 但還保留著兼容, //下個(gè)版本會(huì)去掉
作用于字符串的有:
IS_STR_PERSISTENT //是malloc分配內(nèi)存的字符串 IS_STR_INTERNED //INTERNED STRING IS_STR_PERMANENT //不可變的字符串, 用作哨兵作用 IS_STR_CONSTANT //代表常量的字符串 IS_STR_CONSTANT_UNQUALIFIED //帶有可能命名空間的常量字符串
作用于數(shù)組的有:
#define IS_ARRAY_IMMUTABLE //同IS_TYPE_IMMUTABLE
作用于對象的有:
IS_OBJ_APPLY_COUNT //遞歸保護(hù) IS_OBJ_DESTRUCTOR_CALLED //析構(gòu)函數(shù)已經(jīng)調(diào)用 IS_OBJ_FREE_CALLED //清理函數(shù)已經(jīng)調(diào)用 IS_OBJ_USE_GUARDS //魔術(shù)方法遞歸保護(hù) IS_OBJ_HAS_GUARDS //是否有魔術(shù)方法遞歸保護(hù)標(biāo)志
有了這些預(yù)留的標(biāo)志位, 我們就會(huì)很方便的做一些以前不好做的事情, 就比如我自己的Taint擴(kuò)展, 現(xiàn)在把一個(gè)字符串標(biāo)記為污染的字符串就會(huì)變得無比簡單:
/* it's important that make sure * this value is not used by Zend or * any other extension agianst string */ #define IS_STR_TAINT_POSSIBLE (1<<7) #define TAINT_MARK(str) (GC_FLAGS((str)) |= IS_STR_TAINT_POSSIBLE)
這個(gè)標(biāo)記就會(huì)一直隨著這個(gè)字符串的生存而存在的, 省掉了我之前的很多tricky的做法.
zval預(yù)先分配
前面我們說過, PHP5的zval分配采用的是堆上分配內(nèi)存, 也就是在PHP預(yù)案代碼中隨處可見的MAKE_STD_ZVAL和ALLOC_ZVAL宏. 我們也知道了本來一個(gè)zval只需要24個(gè)字節(jié), 但是算上gc_info, 其實(shí)分配了32個(gè)字節(jié), 再加上PHP自己的內(nèi)存管理在分配內(nèi)存的時(shí)候都會(huì)在內(nèi)存前面保留一部分信息:
typedef struct _zend_mm_block { zend_mm_block_info info; #if ZEND_DEBUG unsigned int magic; # ifdef ZTS THREAD_T thread_id; # endif zend_mm_debug_info debug; #elif ZEND_MM_HEAP_PROTECTION zend_mm_debug_info debug; #endif } zend_mm_block;
從而導(dǎo)致實(shí)際上我們只需要24字節(jié)的內(nèi)存, 但最后竟然分配48個(gè)字節(jié)之多.
然而大部分的zval, 尤其是擴(kuò)展函數(shù)內(nèi)的zval, 我們想想它接受的參數(shù)來自外部的zval, 它把返回值返回給return_value, 這個(gè)也是來自外部的zval, 而中間變量的zval完全可以采用棧上分配. 也就是說大部分的內(nèi)部函數(shù)都不需要在堆上分配內(nèi)存, 它需要的zval都可以來自外部.
于是當(dāng)時(shí)我們做了一個(gè)大膽的想法, 所有的zval都不需要單獨(dú)申請.
而這個(gè)也很容易證明, PHP腳本中使用的zval, 要么存在于符號(hào)表, 要么就以臨時(shí)變量(IS_TMP_VAR)或者編譯變量(IS_CV)的形式存在. 前者存在于一個(gè)Hashtable中, 而在PHP7中Hashtable默認(rèn)保存的就是zval, 這部分的zval完全可以在Hashtable分配的時(shí)候一次性分配出來, 后面的存在于execute_data之后, 數(shù)量也在編譯時(shí)刻確定好了, 也可以隨著execute_data一次性分配, 所以我們確實(shí)不再需要單獨(dú)在堆上申請zval了.
所以, 在PHP7開始, 我們移除了MAKE_STD_ZVAL/ALLOC_ZVAL宏, 不再支持存堆內(nèi)存上申請zval. 函數(shù)內(nèi)部使用的zval要么來自外面輸入, 要么使用在棧上分配的臨時(shí)zval.
在后來的實(shí)踐中, 總結(jié)出來的可能對于開發(fā)者來說最大的變化就是, 之前的一些內(nèi)部函數(shù), 通過一些操作獲得一些信息, 然后分配一個(gè)zval, 返回給調(diào)用者的情況:
static zval * php_internal_function() { ..... str = external_function(); MAKE_STD_ZVAL(zv); ZVAL_STRING(zv, str, 0); return zv; } PHP_FUNCTION(test) { RETURN_ZVAL(php_internal_function(), 1, 1); }
要么修改為, 這個(gè)zval由調(diào)用者傳遞:
static void php_internal_function(zval *zv) { ..... str = external_function(); ZVAL_STRING(zv, str); efree(str); } PHP_FUNCTION(test) { php_internal_function(return_value); }
要么修改為, 這個(gè)函數(shù)返回原始素材:
static char * php_internal_function() { ..... str = external_function(); return str; } PHP_FUNCTION(test) { str = php_internal_function(); RETURN_STRING(str); efree(str); }
總結(jié)
(這塊還沒想好怎么說, 本來我是要引出Hashtable不再存在zval**, 從而引出引用類型的存在的必要性, 但是如果不先講Hashtable的結(jié)構(gòu), 這個(gè)引出貌似很突兀, 先這么著吧, 以后再來修改)
到現(xiàn)在我們基本上把zval的變化概況介紹完畢, 抽象的來說, 其實(shí)在PHP7中的zval, 已經(jīng)變成了一個(gè)值指針, 它要么保存著原始值, 要么保存著指向一個(gè)保存原始值的指針. 也就是說現(xiàn)在的zval相當(dāng)于PHP5的時(shí)候的zval *. 只不過相比于zval *, 直接存儲(chǔ)zval, 我們可以省掉一次指針解引用, 從而提高緩存友好性.
其實(shí)PHP7的性能, 我們并沒有引入什么新的技術(shù)模式, 不過就是主要來自, 持續(xù)不懈的降低內(nèi)存占用, 提高緩存友好性, 降低執(zhí)行的指令數(shù)的這些原則而來的, 可以說PHP7的重構(gòu)就是這三個(gè)原則.
感謝各位的閱讀!關(guān)于PHP7內(nèi)核之zval是什么就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,讓大家可以學(xué)到更多知識(shí)。如果覺得文章不錯(cuò),可以把它分享出去讓更多的人看到吧!