深入剖析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 底層設計與源碼實現》 陳雷等