PHP7已經發布, 如承諾, 我也要開始這個系列的文章的編寫, 今天我想先和你們聊聊zval的變化. 在講zval變化的以前咱們先來看看zval在PHP5下面是什麼樣子php
PHP5
zval回顧
在PHP5的時候, zval的定義以下:node
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內核有了解的同窗應該對這個結構比較熟悉, 由於zval能夠表示一切PHP中的數據類型, 因此它包含了一個type字段, 表示這個zval存儲的是什麼類型的值, 常見的可能選項是IS_NULL, IS_LONG, IS_STRING, IS_ARRAY, IS_OBJECT等等.算法
根據type字段的值不一樣, 咱們就要用不一樣的方式解讀value的值, 這個value是個聯合體, 好比對於type是IS_STRING, 那麼咱們應該用value.str來解讀zval.value字段, 而若是type是IS_LONG, 那麼咱們就要用value.lval來解讀.express
另外, 咱們知道PHP是用引用計數來作基本的垃圾回收的, 因此zval中有一個refcount__gc字段, 表示這個zval的引用數目, 但這裏有一個要說明的, 在5.3之前, 這個字段的名字還叫作refcount, 5.3之後, 在引入新的垃圾回收算法來對付循環引用計數的時候, 做者加入了大量的宏來操做refcount, 爲了能讓錯誤更快的顯現, 因此更名爲refcount__gc, 迫使你們都使用宏來操做refcount.api
相似的, 還有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也不是最最經常使用的類型.ide
第二, 這個結構體的每個字段都有明確的含義定義, 沒有預留任何的自定義字段, 致使在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類型, 這樣就致使了大量的內存浪費.
還好比我以前作的Taint擴展, 我須要對於給一些字符串存儲一些標記, 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);
就是把字符串的長度擴充一個int, 而後用magic number作標記寫到後面去, 這樣的作法安全性和穩定性在技術上都是沒有保障的
第三, 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的時候, 處處都有, 是一個很是常見的用法, 若是咱們能把這個變量用棧分配, 那不管是內存分配, 仍是緩存友好, 都是很是有利的
還有不少, 我就不一一詳細列舉了, 可是我相信大家也有了和咱們當時同樣的想法, zval必須得改改了, 對吧?
PHP7
如今的zval
到了PHP7中, zval變成了以下的結構, 要說明的是, 這個是如今的結構, 已經和PHPNG時候有了一些不一樣了, 由於咱們新增長了一些解釋 (聯合體的字段), 可是整體大小, 結構, 是和PHPNG的時候一致的:
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; };
雖然看起來變得好大, 但其實你仔細看, 所有都是聯合體, 這個新的zval在64位環境下,如今只須要16個字節(2個指針size), 它主要分爲倆個部分, value和擴充字段, 而擴充字段又分爲u1和u2倆個部分, 其中u1是type info, u2是各類輔助字段.
其中value部分, 是一個size_t大小(一個指針大小), 能夠保存一個指針, 或者一個long, 或者一個double.
而type info部分則保存了這個zval的類型. 擴充輔助字段則會在多個其餘地方使用, 好比next, 就用在取代Hashtable中原來的拉鍊指針, 這部分會在之後介紹HashTable的時候再來詳解.
類型
PHP7中的zval的類型作了比較大的調整, 整體來講有以下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的時候的IS_BOOL類型, 如今拆分紅了IS_FALSE和IS_TRUE倆種類型. 而原來的引用是一個標誌位, 如今的引用是一種新的類型.
對於IS_INDIRECT和IS_PTR來講, 這倆個類型是用在內部的保留類型, 用戶不會感知到, 這部分會在後續介紹HashTable的時候也一併介紹.
從PHP7開始, 對於在zval的value字段中能保存下的值, 就再也不對他們進行引用計數了, 而是在拷貝的時候直接賦值, 這樣就省掉了大量的引用計數相關的操做, 這部分類型有:
IS_LONG IS_DOUBLE
固然對於那種根本沒有值, 只有類型的類型, 也不須要引用計數了:
IS_NULL IS_FALSE IS_TRUE
而對於複雜類型, 一個size_t保存不下的, 那麼咱們就用value來保存一個指針, 這個指針指向這個具體的值, 引用計數也隨之做用於這個值上, 而不在是做用於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將指向上面的這樣的一個結構體, 由它實際保存一個數組, 引用計數部分保存在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*結構來處理.
另外有一個須要說明的就是你們可能會好奇的ZEND_ENDIAN_LOHI_4宏, 這個宏的做用是簡化賦值, 它會保證在大端或者小端的機器上, 它定義的字段都按照同樣順序排列存儲, 從而咱們在賦值的時候, 不須要對它的字段分別賦值, 而是能夠統一賦值, 好比對於上面的array結構爲例, 就能夠經過:
arr1.u.flags = arr2.u.flags;
一次完成至關於以下的賦值序列:
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;
還有一個你們可能會問到的問題是, 爲何不把type類型放到zval類型的前面, 由於咱們知道當咱們去用一個zval的時候, 首先第一點確定是先去獲取它的類型. 這裏的一個緣由是, 一個是倆者差異不大, 另外就是考慮到若是之後JIT的話, zval的類型若是可以經過類型推導得到, 就根本沒有必要去讀取它的type值了.
標誌位
除了數據類型之外, 之前的經驗也告訴咱們, 一個數據除了它的類型之外, 還應該有不少其餘的屬性, 好比對於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的.
那麼相似這樣的標誌位一共有多少呢?做用於zval的有:
IS_TYPE_CONSTANT //是常量類型 IS_TYPE_IMMUTABLE //不可變的類型, 好比存在共享內存的數組 IS_TYPE_REFCOUNTED //須要引用計數的類型 IS_TYPE_COLLECTABLE //可能包含循環引用的類型(IS_ARRAY, IS_OBJECT) IS_TYPE_COPYABLE //可被複制的類型, 還記得我以前講的對象和資源的例外麼? 對象
和資源就不是
IS_TYPE_SYMBOLTABLE //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 //是否有魔術方法遞歸保護標誌
有了這些預留的標誌位, 咱們就會很方便的作一些之前很差作的事情, 就好比我本身的Taint擴展, 如今把一個字符串標記爲污染的字符串就會變得無比簡單:
/* 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)
這個標記就會一直隨着這個字符串的生存而存在的, 省掉了我以前的不少tricky的作法.
ZVAL預先分配
前面咱們說過, PHP5的zval分配採用的是堆上分配內存, 也就是在PHP預案代碼中隨處可見的MAKE_STD_ZVAL和ALLOC_ZVAL宏. 咱們也知道了原本一個zval只須要24個字節, 可是算上gc_info, 其實分配了32個字節, 再加上PHP本身的內存管理在分配內存的時候都會在內存前面保留一部分信息:
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;
從而致使實際上咱們只須要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.
在後來的實踐中, 總結出來的可能對於開發者來講最大的變化就是, 以前的一些內部函數, 經過一些操做得到一些信息, 而後分配一個zval, 返回給調用者的狀況:
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); }
要麼修改成, 這個zval由調用者傳遞:
static void php_internal_function(zval *zv) { ..... str = external_function(); ZVAL_STRING(zv, str); efree(str); } PHP_FUNCTION(test) { php_internal_function(return_value); }
要麼修改成, 這個函數返回原始素材:
static char * php_internal_function() { ..... str = external_function(); return str; } PHP_FUNCTION(test) { str = php_internal_function(); RETURN_STRING(str); efree(str); }
總結
(這塊還沒想好怎麼說, 原本我是要引出Hashtable再也不存在zval**, 從而引出引用類型的存在的必要性, 可是若是不先講Hashtable的結構, 這個引出貌似很突兀, 先這麼着吧, 之後再來修改)
到如今咱們基本上把zval的變化概況介紹完畢, 抽象的來講, 其實在PHP7中的zval, 已經變成了一個值指針, 它要麼保存着原始值, 要麼保存着指向一個保存原始值的指針. 也就是說如今的zval至關於PHP5的時候的zval . 只不過相比於zval , 直接存儲zval, 咱們能夠省掉一次指針解引用, 從而提升緩存友好性.
其實PHP7的性能, 咱們並無引入什麼新的技術模式, 不過就是主要來自, 持續不懈的下降內存佔用, 提升緩存友好性, 下降執行的指令數的這些原則而來的, 能夠說PHP7的重構就是這三個原則.