首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

深入解析 PHP 7 變數內部實現(二):複雜類型與優化細節

     編輯:PHP 相關知識

PHP 7 變數內部實現深度解析(二)

承接上一篇「變數在 PHP 7 內部的實現(一)」,本文將繼續深入探討 PHP 7 核心中關於變數的實現細節。內容主要參考自 PHP 核心開發者 Nikita Popov (nikic) 的技術部落格,並經過整理以符合中文閱讀習慣。

要理解本文,建議先對 PHP 5 中變數的實現有一定了解,重點在於解釋 PHP 7 中 zval 結構的重大變化。

核心變化回顧

PHP 7 最大的變革在於 zval 不再單獨分配記憶體,也不再自行儲存引用計數。標量類型(整數、浮點數等)直接內嵌於 zval 中;複雜類型則透過指標指向獨立的結構體。這種設計大幅減少了記憶體分配開銷,提升了效能。

所有複雜類型的 zval 資料值共享一個通用頭部,由 zend_refcounted 定義:

struct _zend_refcounted {
    uint32_t refcount;
    union {
        struct {
            ZEND_ENDIAN_LOHI_3(
                zend_uchar type,
                zend_uchar flags,
                uint16_t gc_info
            )
        } v;
        uint32_t type_info;
    } u;
};

此頭部包含引用計數 refcount、類型 type、垃圾回收資訊 gc_info 以及類型標誌位 flags。

字串實現:zend_string

PHP 7 引入全新結構體 zend_string 專門儲存字串變數:

struct _zend_string {
    zend_refcounted gc;
    zend_ulong h;        /* hash 值快取 */
    size_t len;          /* 字串長度 */
    char val[1];         /* 字串實際內容(柔性陣列) */
};

除了引用計數頭部,字串還包含哈希快取 h(避免重複計算哈希值)、長度 len 以及內容 val。注意 val[1] 這種宣告方式:實際建立時會分配足夠記憶體儲存完整字串,這是 C 語言常見的「結構體駭客」技巧,解決 Windows 平台不支援 C99 柔性陣列的問題。

新字串類型優勢明顯:直接儲存長度避免重複計算;擁有引用計數可直接共享字串內容(如 HashTable 的 key),無需透過 zval。但缺點是從 C 字串轉為 zend_string 需額外分配記憶體並複製,使用上稍顯繁瑣。

字串特有標誌位(儲存於 GC 的 flags 中):

#define IS_STR_PERSISTENT (1<<0) /* 使用 malloc 分配,持久化 */
#define IS_STR_INTERNED   (1<<1) /* 內部字串,請求結束銷毀 */
#define IS_STR_PERMANENT  (1<<2) /* 永久內部字串,跨請求存活 */
  • 持久化字串:直接從系統分配記憶體,不經 Zend 記憶體管理器(ZMM),生命週期超越單次請求。
  • 內部字串:請求期間唯一且不可變,無需引用計數。PHP 原始碼中不可變字串(常量、變數名、函數名等)皆為內部字串。
  • 永久內部字串:啟用 OPcache 時儲存於共享記憶體(SHM),全進程共享且永不銷毀,此時持久化字串失去意義。

陣列實現

新陣列實現已在先前文章詳細闡述,此處補充不可變陣列概念:本質類似內部字串,無引用計數,請求期間常駐(啟用 OPcache 時甚至跨請求存活)。僅在啟用 OPcache 時使用,主要為了解決記憶體管理便利性。

實例對比:

<?php
for ($i = 0; $i < 1000000; ++$i) {
    $array[] = ['foo'];
}
var_dump(memory_get_usage());

啟用 OPcache 時僅佔 32MB;未啟用則需 390MB,因為每次皆完整複製 ['foo'] 而非增加引用計數,這是為防止 Zend VM 執行時共享記憶體出錯。期望未來改善無 OPcache 時的記憶體暴增問題。

物件實現:PHP 5 vs PHP 7

PHP 5 物件架構

PHP 5 中 zval 儲存 zend_object_value:

typedef struct _zend_object_value {
    zend_object_handle handle;
    const zend_object_handlers *handlers;
} zend_object_value;

handle 為物件唯一 ID,用於查找物件資料;handlers 為虛函數表指標。物件實際儲存於「物件存儲」(Object Store)中,其 bucket 結構包含解構、釋放、複製等操作處理器,以及雙重引用計數(zval 與 bucket 各自計數),導致深度複製時需同步兩個計數器,增加複雜度。

實際物件結構 zend_object 包含類別項目、屬性表、動態屬性 HashTable 及魔術方法守衛表。最小物件需 136 字節,且存取屬性需經歷 4~7 層間接指標,效能損耗明顯。

PHP 7 物件優化

新 zend_object 結構:

struct _zend_object {
    zend_refcounted gc;
    uint32_t handle;
    zend_class_entry *ce;
    const zend_object_handlers *handlers;
    HashTable *properties;
    zval properties_table[1];  /* 柔性陣列:物件與屬性表共享記憶體塊 */
};

關鍵改進:

  • 移除雙重引用計數,僅保留 zend_refcounted 頭部。
  • handle、handlers 直接內嵌,省去物件存儲查找。
  • 屬性表 properties_table 以柔性陣列方式內嵌於物件結構末尾,物件與屬性共享同一記憶體塊,大幅減少間接存取。
  • 移除 guards 表,改為按需儲存於屬性表首位(僅使用魔術方法時)。
  • 解構、釋放、複製操作處理器移至 handlers 表:
struct _zend_object_handlers {
    int offset;                    /* 物件頭部偏移量 */
    zend_object_free_obj_t free_obj;
    zend_object_dtor_obj_t dtor_obj;
    zend_object_clone_obj_t clone_obj;
    /* ... 其他成員同 PHP 5 */
};

offset 解決 C 語言結構體繼承問題:PHP 7 將自訂成員置於標準 zend_object 之前,需透過偏移量計算實際位址。

雖然物件指標直接儲存於 zval,但物件存儲仍保留(簡化為指標陣列),handle 仍為索引。主要原因:請求關閉階段需提前執行所有物件解構函數,需活躍物件列表;除外 handle 為除錯提供唯一 ID,便於區分物件身分。

效能提升:基礎物件僅 40 字節,每屬性 16 字節;屬性存取從 4 層間接降為 1 層。

間接 zval(IS_INDIRECT)

PHP 7 新增特殊類型 IS_INDIRECT,表示真實值儲存於他處。與 IS_REFERENCE 不同,間接 zval 直接指向另一個 zval 而非包裝結構。

用途:編譯期已知變數儲存於 CV (Compiled Variables) 表;動態變數(如 $GLOBALS)使用符號表。PHP 5 採用雙重指標 zval** 同步兩表;PHP 7 因 HashTable 擴容導致 bucket 失效,改為符號表儲存 INDIRECT 指向 CV 表(CV 表在符號表生命週期內不重新分配)。

範例:函數擁有 CV 變數 $a、$b、$c 及動態 $d:

SymbolTable["a"].value = INDIRECT → CV[0] = LONG 42
SymbolTable["b"].value = INDIRECT → CV[1] = DOUBLE 42.0
SymbolTable["c"].value = INDIRECT → CV[2] = STRING → zend_string("42")
SymbolTable["d"].value = ARRAY → zend_array([4, 2])

間接 zval 可指向 IS_UNDEF,配合 unset() 判斷符號表鍵值是否存在。

常量與 AST:IS_CONSTANT、IS_CONSTANT_AST

支援「延遲綁定」:函數參數預設值、類別靜態屬性等靜態表達式,於首次使用時才解析。未定義常數值時儲存為 IS_CONSTANT;若為表達式則用 IS_CONSTANT_AST 指向抽象語法樹(AST)。

<?php
function test($a = ANSWER, $b = ANSWER * ANSWER) {
    return $a + $b;
}
define('ANSWER', 42);
var_dump(test()); // int(42 + 42 * 42)

總結

本文系統梳理了 PHP 7 複雜類型(字串、陣列、物件)的內部實現,以及間接 zval、常量延遲綁定等特殊機制。相比 PHP 5,PHP 7 透過記憶體佈局優化、移除雙重引用計數、減少間接存取層級,實現了顯著的效能與記憶體提升。後續將探討 VM 優化、命名慣例及編譯器基礎建設優化。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved