深入剖析PHP7內核源碼(二)- PHP變數容器

  • 2019 年 10 月 3 日
  • 筆記

簡介

PHP的變數使用起來非常方便,其基本結構是底層實現的zval,PHP7採用了全新的zval,由此帶來了非常大的性能提升,本文重點分析PHP7的zval的改變。

PHP5時代的ZVAL

typedef struct _zval_struct {      zvalue_value value; // (長度16位元組,具體看下面的分析)      zend_uint refcount__gc; //  unsigned int (長度4位元組)      zend_uchar type;  //  unsigned char (長度1位元組)      zend_uchar is_ref__gc;  //  unsigned char (長度1位元組)  } zval      typedef union _zvalue_value {      long lval;                 // 用於 bool 類型、整型和資源類型(長度8位元組)      double dval;               // 用於浮點類型(長度8位元組)      struct {                   // 用於字元串          char *val;    // 字元串指針(長度8位元組)          int len;   //字元串長度(長度4位元組)      } str;      HashTable *ht;             // 用於數組(長度8位元組)      zend_object_value obj;     // 用於對象(12位元組)      zend_ast *ast;             // 用於常量表達式(長度8位元組)  } zvalue_value;
  • zvalue_value 是聯合體,長度取最大的一個,為12位元組,記憶體對齊後是16位元組(需要對齊為8的倍數)。
  • zval 是結構體,長度是各個變數的總和,為22位元組,記憶體對齊後是24位元組。
  • php5.3後對zval進行了擴充,解決循環引用的問題,因此實際上申請一個變數分配了 24 + 8 = 32位元組的記憶體。
typedef struct _zval_gc_info {      zval z;      union {          gc_root_buffer       *buffered;          struct _zval_gc_info *next;      } u;   // (長度8位元組)  } zval_gc_info;

所以在PHP裡面,給一個變數賦值,實際上會轉換成這樣來運行

<?php  $var = 123  =>  zval.value = 123  zval.type = IS_LONG  zval.refcount__gc= 0  zval.is_ref__gc = 0  ... 

PHP7 時代的ZVAL

struct _zval_struct {       union {            zend_long         lval;   // 整型(長度8位元組)            double            dval;       // 浮點型(長度8位元組)            zend_refcounted  *counted; // 引用計數(長度8位元組)            zend_string      *str; // 字元串類型(長度8位元組)            zend_array       *arr; // 數組(長度8位元組)            zend_object      *obj; // 對象(長度8位元組)            zend_resource    *res; // 資源型(長度8位元組)            zend_reference   *ref; // 引用型(長度8位元組)            zend_ast_ref     *ast; //抽象語法樹(長度8位元組)            zval             *zv; // zval類型(長度8位元組)            void             *ptr; // 指針類型(長度8位元組)            zend_class_entry *ce; // class類型(長度8位元組)            zend_function    *func; // function類型(長度8位元組)            struct {                 uint32_t w1;   // (長度4位元組)                 uint32_t w2;  // (長度4位元組)            } ww;  // 長度8位元組       } value;  // 因為是聯合體,所以實際上整個value只用了8位元組        union {          struct {              ZEND_ENDIAN_LOHI_4(                  zend_uchar    type,       // zval的類型(長度1位元組)                  zend_uchar    type_flags, //對應變數類型特有的標記(長度1位元組)                  zend_uchar    const_flags, // 常量類型標記(長度1位元組)                  zend_uchar    reserved)     // 保留欄位(長度1位元組)          } v; // 總共長度是4位元組          uint32_t type_info; //  其實就是v的值位運算結果(長度4位元組)      } u1; // u1也是聯合體,總共長度4位元組        union {          uint32_t     var_flags;          uint32_t     next;                 // 用來解決哈希衝突的(長度4位元組)          uint32_t     cache_slot;      // 運行時快取(長度4位元組)          uint32_t     lineno;              // zend_ast_zval行號(長度4位元組)          uint32_t     num_args;         // Ex(This) 參數個數(長度4位元組)          uint32_t     fe_pos;              // foreach 的位置(長度4位元組)          uint32_t     fe_iter_idx;         // foreach 迭代器游標(長度4位元組)      } u2;  // u2也是聯合體,總共長度4位元組  };
  • value (8) + u1(4) +u2(4) = 16,整個變數才用了16位元組,相比PHP5來說,節省了一半記憶體。
  • value 保存具體是值,不同的類型的值,用的是聯合體的同一塊空間。
  • u1 變數的類型就通過u1.v.type區分,另外一個值type_flags為類型掩碼,在變數的記憶體管理、gc機制中會用到
  • u2 輔助值,假如zval只有:value、u1兩個值,整個zval的大小也會對齊到16byte,所以加了u2作為輔助,比如next在哈希表解決哈希衝突時會用到,還有fe_pos在foreach會用到

zvalue的類型

zvalue.u1.type

/* 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_AST             11    /* internal types (偽類型)*/  #define IS_INDIRECT                 13  #define IS_PTR                      14  #define _IS_ERROR                   15    /* fake types used only for type hinting (Z_TYPE(zv) can not use them)  內部類型*/  #define _IS_BOOL                    16  #define IS_CALLABLE                 17  #define IS_ITERABLE                 18  #define IS_VOID                     19  #define _IS_NUMBER                  20
  • PHP是根據u1.v.type的類型取不同的值,比如u1.v.type == IS_LONG,則取值 value.lval
  • IS_UNDEF 未定義,表示數據可以被刪除,可用於對數組unset的時候標記Bucket的位置為IS_UNDEF,等標記元素達到閾值的時候,進行rehash操作刪除數據
  • IS_TRUE IS_FALSE 將PHP5時代的IS_BOOL分開為兩個,只需要一次操作即可取值。
  • IS_REFERENCE 處理&變數
  • IS_INDIRECT 解決全局符號表訪問CV變數表
  • IS_PTR 指針類型,解釋 value.ptr,通常用在函數類型上,比如聲明一個函數
  • _IS_ERROR 檢查zval的類型是否合法

字元串的實現

struct _zend_string {      zend_refcounted_h gc;  // 引用計數,變數引用資訊      zend_ulong        h;          // 哈希值,數組中計算索引時會用到      size_t            len; // 字元串長度      char              val[1]; //  字元串內容  };
  • zend_ulong h 快取了字元串的hash值,避免了數組中的重複計算字元串hash,提升了5%的性能
  • val值儲存字元串類型,用的是柔性數組類型

zval.value->gc.u.flags 這個標記代表了下面幾種不同類型的字元串

IS_STR_PERSISTENT(通過malloc分配的)  IS_STR_INTERNED(php程式碼里寫的一些字面量,比如函數名、變數值)  IS_STR_PERMANENT(永久值,生命周期大於request)  IS_STR_CONSTANT(常量)  IS_STR_CONSTANT_UNQUALIFIED

整數的實現

整數是標量,在容器中zval直接存儲

$a = 666;  // $a = zval_1(u1.v.type=IS_LONG,value.lval=666)    $b = $a;  // $a = zval_1(u1.v.type=IS_LONG,value.lval=666)  // $b = zval_2(u1.v.type=IS_LONG,value.lval=666)    unset($a);  // $a = zval_1(u1.v.type=IS_UNDEF,value.lval=666)
  • PHP7相對於PHP5 的一個改變就是,對標量的值直接拷貝,而沒有做寫時拷貝,因為zval只有16位元組,寫時拷貝實際上節省不了記憶體還會增加操作的複雜度。
  • unset的時候把 u1.v.type 標記為IS_UNDEF,記憶體不會釋放。

數組的全貌

數組的基本結構是基於key value的 HashTable,同時是一個雙向鏈表。熟悉數據結構的都知道,對一個字元串Hash的時候有可能產生哈希衝突,PHP是怎麼解決的?當發生衝突的時候,PHP在該映射後面會加上一條鏈表,哈希衝突後就會從鏈表中找值。使用了雙向鏈表的好處是,我們對數組最常用的操作就是遍曆數組,通過雙向鏈表,我們可以很方便進行遍歷。你可能會問,那如果僅僅是這樣,單向鏈表不也解決了嗎?還節省點空間。實際上,之所以用雙向鏈表的一個原因,是因為鏈表在刪除元素的時候,就必須找到上一個元素,把它的指針指向到下下個元素,雙向鏈表已經儲存了上一個元素的指針,而單向鏈表就必須遍歷整個HashTable,時間複雜度將會是很差的O(n)。

  • HashTable刪除元素的時間複雜度是O(1),雙向鏈表刪除的時間複雜度也是O(1),所以整個刪除操作可以做到時間最優的O(1)。

這個是PHP數組的大概樣子,後面會專門寫一篇來概述是數組HashTable的實現。

資源類型

PHP中很多依賴外部的操作都是資源類型,比如文件資源 Socket連接資源,資源類型的定義如下

struct _zend_resource{      zend_refcounted_h gc;      int    handle;      int    type;      void    *ptr; //指針,根據使用場景轉換為任何類型  }

對象類型

struct _zend_object {      zend_refcounted_h gc;      uint32_t          handle;      zend_class_entry *ce; //對象對應的class類      const zend_object_handlers *handlers;      HashTable        *properties; //對象屬性哈希表      zval              properties_table[1];  };

properties 是一個HashTable ,key 對象的屬性 ,value是對象在properties_table 數組中的偏移量,值真正的位置是在properties_table 數組中。

引用類型

PHP的引用類型是比較特殊的一種類型,可以通過 & 操作符可以產生一個引用變數,假如把 $b = &a; $b 的值改變的時候,$a 的值也跟著改變。

struct _zend_reference {      zend_refcounted_h gc;      zval              val;  };
  • zend_refcounted_h 結構體用來儲存引用計數的資訊
  • val 存儲的是實際的值
$a = "time:" . time();      //$a    -> zend_string_1(refcount=1)  $b = &$a;                 //$a,$b -> zend_reference_1(refcount=2) -> zend_string_1(refcount=1)  $c = $b;                  //$a,$b -> zend_reference_1(refcount=2) -> zend_string_1(refcount=2)                                 //$c    ->   zend_string_1(refcount=2)
  1. $a 賦值字元串,zend_string_1 的引用計數記為1。
  2. 把$a的引用賦值給$b,zend_string_1 結構的引用計數不變,產生了一個中間結構體zend_reference_1,該結構體的引用計數為2。
  3. $b 賦值給$c ,zend_reference_1引用計數不變,zend_string_1引用計數記為2。

中間結構體zend_reference_1存在的好處是,zend_string只需要存一份,減少空間的浪費以及申請空間帶來的額外開銷

附錄

什麼是記憶體對齊

比如數據匯流排有32位,它訪存只能4個位元組4個位元組地進行。 0-3,4-7,8-11,12-15,…… 即使我們需要的數據只佔一個位元組,也是一次讀取4個位元組。 一個位元組的數據不管地址是什麼,都能通過一次訪存讀取出來。 而如果要讀取的數據是一個位元組以上,比如兩個位元組, 如果該數據的記憶體地址是0x03,則需要兩次才能讀取該數據, 第一次讀0x00-0x03,第二次讀0x04-0x07。 這個數據就跨越了訪存邊界。而相對CPU的運算來說,訪存是非常慢的,所以要盡量減少訪存次數。 為了減少跨越訪存邊界的數據引起的訪存開銷, 所以編譯器會進行記憶體對齊,即把變數的地址做一些偏移, 目的是一次訪存就讀出數據,不然的話也要以儘可能少地訪存次數讀出數據。如上一個例子中那樣,整型成員i的地址做4個位元組的偏移, 而Sample對象的地址也會做4位元組邊界的對齊, 這樣i的地址始終是4的倍數,從而使得i不跨越訪存邊界, 能一次讀出它的值。

typedef struct{      char a;      char b;      int i;  } Sample1;

Sample1佔多少空間呢?仍然是8個位元組。 a在第0個位元組,b在第1個位元組,i佔4-7位元組。 這是記憶體對齊的原則,佔用盡量少的記憶體。 如果在b之後,還有char類型的成員c和d,同樣是佔8個位元組。 a,b,c,d在0-3位元組。

引用