PHP 7 帶來了革命性的性能提升,其核心在於變量存儲機制的徹底重寫。本文將深入剖析 PHP 5 與 PHP 7 在 zval 結構、引用計數、內存管理、類型系統及引用實現上的本質區別,助你理解底層原理,寫出更高效的代碼。
在 PHP 5 中,每個變量都是一個獨立堆分配的 zval 結構體:
typedef struct _zval_struct {
zvalue_value value; // 聯合體:存儲實際數據
zend_uint refcount__gc; // 引用計數
zend_uchar type; // 類型標識
zend_uchar is_ref__gc; // 是否為 PHP 引用(&)
} zval;
zvalue_value 聯合體包含 lval(長整型/布爾/資源)、dval(雙精度)、str(字符串)、ht(數組)、obj(對象)等。由於聯合體特性,zval 體積固定為「最大成員大小 + 對齊」,在 64 位系統上佔用 24 字節,加上 zval_gc_info(循環 GC 信息)共 32 字節,再加上堆分配元數據 16 字節,每個 zval 實際佔用 48 字節。
int 只需 8 字節,卻強制分配 48 字節。zval* -> zvalue_value.obj -> zend_object* -> 實際數據 四層指針跳轉。PHP 7 採用「值語義 + 共享實體」設計:
emalloc。zend_refcounted 頭部,簡單類型(整型、浮點、布爾、null)完全無計數開銷。zval -> zend_string/array/object* 直接指向帶計數的實體,減少一次間接。struct _zval_struct {
zend_value value; // 8 字節聯合體:lval/dval/指針
union {
struct {
zend_uchar type; // 活躍類型
zend_uchar type_flags; // 标志:可計數、可回收、可複製、不可變等
zend_uchar const_flags;
zend_uchar reserved;
} v;
uint32_t type_info; // 合併存儲,便於原子操作
} u1;
union { // 輔助字段(緩存槽、行號、foreach 位置等)
uint32_t var_flags;
uint32_t next;
uint32_t cache_slot;
uint32_t lineno;
uint32_t num_args;
uint32_t fe_pos;
uint32_t fe_iter_idx;
} u2;
};
關鍵點:zend_value 僅 8 字節(64 位指針大小),type_info 32 位足夠存放類型與標誌,整個 zval 僅 16 字節(對齊後),比 PHP 5 縮小 2/3。
typedef struct _zend_refcounted_h {
uint32_t refcount; // 32 位引用計數
union {
struct {
zend_uchar type; // 類型(與 zval.type 一致)
zend_uchar flags; // 類型專用標誌(字符串:是否持久/可變;對象:是否實例化等)
uint16_t gc_info; // GC 根緩衝區索引 + 顏色標記(取代 64 位指針)
} v;
uint32_t type_info;
} u;
} zend_refcounted_h;
所有復雜類型(zend_string、zend_array、zend_object 等)的內存佈局均以此結構開頭,實現「計數隨數據走」。
PHP 7 通過位掩碼精細控制行為:
| 標誌 | 宏定義 | 含義 |
|---|---|---|
| 1<<0 | IS_TYPE_CONSTANT | 常量表達式專用 |
| 1<<1 | IS_TYPE_IMMUTABLE | 不可變(如靜態數組、interned string) |
| 1<<2 | IS_TYPE_REFCOUNTED | 需要引用計數 |
| 1<<3 | IS_TYPE_COLLECTABLE | 參與循環 GC(數組/對象) |
| 1<<4 | IS_TYPE_COPYABLE | 賦值時需深度複製(數組/字符串) |
| 1<<5 | IS_TYPE_SYMBOLTABLE | 符號表專用 |
類型與標誌對應表:
| 類型 | 可計數 | 可回收 | 可複製 | 不可變 |
|---|---|---|---|---|
| 簡單類型 | ||||
| 字符串 | ✓ | ✓ | ||
| Interned String | ✓ | |||
| 數組 | ✓ | ✓ | ✓ | |
| 不可變數組 | ✓ | |||
| 對象 | ✓ | ✓ | ||
| 資源 | ✓ | |||
| 引用 | ✓ |
$a = 42; // zval_1(type=IS_LONG, value=42) $b = $a; // zval_2(type=IS_LONG, value=42) 直接複製 16 字節 $a += 1; // zval_1(value=43) 原地修改,無分離 unset($a); // zval_1 標記 IS_UNDEF
不再有「寫時複製」開銷,整數賦值僅是 16 字節內存拷貝,極快。
$a = []; // zval_1 -> zend_array_1(refcount=1)
$b = $a; // zval_2 -> zend_array_1(refcount=2) 僅增加計數
$a[] = 1; // 分離:zval_1 -> zend_array_2(refcount=1, [1])
// zval_2 -> zend_array_1(refcount=1, [])
unset($a); // zend_array_2 計數歸零銷毀,zval_1 標 IS_UNDEF
行為與 PHP 5 類似,但分離操作僅針對 zend_array,zval 本身無需分配/釋放。
/* 常規類型 */ #define IS_UNDEF 0 // 取代 NULL 指針,配合 unset #define IS_NULL 1 #define IS_FALSE 2 // 拆分 IS_BOOL,優化類型檢查 #define IS_TRUE 3 #define IS_LONG 4 // zend_long,64 位有符號(Windows 也支援 64 位整數) #define IS_DOUBLE 5 #define IS_STRING 6 #define IS_ARRAY 7 #define IS_OBJECT 8 #define IS_RESOURCE 9 #define IS_REFERENCE 10 // 新增:PHP 引用作為獨立類型 /* 常量表達式 */ #define IS_CONSTANT 11 #define IS_CONSTANT_AST 12 /* 內部類型 */ #define IS_INDIRECT 15 // 間接引用(如符號表佔位) #define IS_PTR 17 // 任意指針包裝
Z_TYPE_P(zv) == IS_TRUE 比 PHP 5 的 Z_TYPE_P(zv) == IS_BOOL && Z_LVAL_P(zv) 快一倍。long 只有 32 位的問題,PHP 7 在 64 位 Windows 也能用 64 位整數。$a = []; // zval_1(refcount=1, is_ref=0) -> HT_1
$b = $a; // zval_1(refcount=2, is_ref=0) -> HT_1
$c =& $b; // 必須複製:zval_1(refcount=2, is_ref=0) -> HT_1
// zval_2(refcount=2, is_ref=1) -> HT_2 (copy of HT_1)
$c[] = 1; // 只修改 HT_2,$a/$b 不受影響
更糟的是 count($ref) 會觸發整個大數組的深度複製,性能災難。
struct _zend_reference {
zend_refcounted gc; // 計數頭
zval val; // 內嵌真實值(可以是數組指針)
};
所有引用變量指向同一個 zend_reference(類型 IS_REFERENCE),其內部 val 指向共享的復雜數據(如數組)。
$a = []; // zval_a -> zend_array_1(rc=1)
$b = $a; // zval_b -> zend_array_1(rc=2)
$c = $b; // zval_c -> zend_array_1(rc=3)
$d =& $c; // zval_d, zval_c -> zend_reference_1(rc=2) -> zend_array_1(rc=3)
$d[] = 1; // 分離數組:zend_reference_1 -> zend_array_2(rc=1, [1])
// zval_a, zval_b -> zend_array_1(rc=2, [])
關鍵優勢:
count($ref) 不再複製。zend_reference 分配開銷,但比 PHP 5 全量複製快數量級。emalloc/efree,CPU 緩存友好。下篇將深入剖析字符串(zend_string)、數組(zend_array)、對象(zend_object)等具體類型的內存佈局與優化細節,敬請期待。