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

深入剖析PHP7內核源碼(二)- PHP變量容器
簡介
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)

$a 賦值字符串,zend_string_1 的引用計數記爲1。
把$a的引用賦值給$b,zend_string_1 結構的引用計數不變,產生了一箇中間結構體zend_reference_1,該結構體的引用計數爲2。
$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字節。

引用
深入理解PHP7內核之zval http://www.laruence.com/2018/04/08/3170.html
C語言的內存對齊 https://www.cnblogs.com/jiqingwu/p/4043338.html
php7-internal https://github.com/pangudashu/php7-internal/blob/master/2/zval.md
《PHP7 底層設計與源碼實現》 陳雷等

原文地址https://www.cnblogs.com/jaychan/p/11261404.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章