PHP7 核心 Object 深入理解

2020-07-16 10:06:15

PHP5

按照慣例,我先帶大家回顧下PHP5時的zend_object(此部分內容之前的文章中也有涉及,如果熟悉可以跳過), 之前如果有興趣也可以看看我10年前寫的深入理解PHP原理之物件.

PHP5中,物件的定義如下:

typedef struct _zend_object {
    zend_class_entry *ce;
    HashTable *properties;
    zval **properties_table;
    HashTable *guards;
} zend_object;

其中ce儲存了這個物件所屬的類, 關於properties_table和properties, properties_table是申明的屬性,properties是動態屬性,也就是比如:

<?php
class Foo {
    public $a = 'defaul property';
}
$a = New Foo();
$a->b = 'dynamic property';

因為在Foo的定義中,我們申明了public $a, 那麼$a就是已知的申明屬性,它的可見性,包括在properties_table中儲存的位置都是在申明後就確定的。

而$a->b, 是我們動態給新增的屬性,它不屬於已經申明的屬性,這個會儲存在properties中。

其實從型別上也能看出來, properties_table是zval*的陣列,而properties是Hashtable。

guards主要用在魔術方法呼叫的時候巢狀保護, 比如__isset/__get/__set。

總體來說, zend_object(以下簡稱object)在PHP5中其實是一種相對特殊的存在, 在PHP5中,只有resource和object是參照傳遞,也就是說在賦值,傳遞的時候都是傳遞的本身,也正因為如此,Object和Resource除了使用了Zval的參照計數以外,還採用了一套獨立自身的計數系統。

這個我們從zval中也能看出object和其他的類似字串的的不同:

typedef union _zvalue_value {
    long lval;
    double dval;
    struct {
        char *val;
        int len;
    } str;
    HashTable *ht;
    zend_object_value obj;
} zvalue_value;

對於字串和陣列,zval中都直接儲存它們的指標,而對於object卻是一個zend_object_value的結構體:

typedef unsigned int zend_object_handle;

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

真正獲取物件是需要通過這個zend_object_handle,也就是一個int的索引去全域性的object buckets中查詢:

ZEND_API void *zend_object_store_get_object_by_handle(zend_object_handle handle TSRMLS_DC)
{
    return EG(objects_store).object_buckets[handle].bucket.obj.object;
}

而EG(objects_store).object_buckets則是一個陣列,儲存著:

typedef struct _zend_object_store_bucket {
    zend_bool destructor_called;
    zend_bool valid;
    zend_uchar apply_count;
    union _store_bucket {
        struct _store_object {
            void *object;
            zend_objects_store_dtor_t dtor;
            zend_objects_free_object_storage_t free_storage;
            zend_objects_store_clone_t clone;
            const zend_object_handlers *handlers;
            zend_uint refcount;
            gc_root_buffer *buffered;
        } obj;
        struct {
            int next;
        } free_list;
    } bucket;
} zend_object_store_bucket;

其中,zend_object_store_bucket.bucket.obj.object才儲存著真正的zend_object的指標,注意到此處是void *, 這是因為我們很多擴充套件的自定義物件,也是可以儲存在這裡的。

另外我們也注意到zend_object_store_bueckt.bucket.obj.refcount, 這個既是我剛剛講的object自身的參照計數,也就是zval有一套自己的參照計數,object也有一套參照計數。

<?php
$o1 = new Stdclass();
//o1.refcount == 1, object.refcount == 1
$o2 = $o1;
//o1.refcount == o2.refcoun == 2; object.refcount = 1;
$o3 = &$o2;
//o3.isref == o2.isref==1
//o3.refcount == o2.refcount == 2
//o1.isref == 0; o1.refcount == 1
//object.refcount == 2

這樣,可以讓object可以保證不同於普通的zval的COW機制,可以保證object可以全域性傳參照。

可見,從一個zval到取到實際的object,我們需要首先獲取zval.value.obj.handle, 然後拿著這個索引再去EG(objects_store)查詢,效率比較低下。

對於另外一個常見的操作,就是獲取一個zval物件的類的時候,我們也需要需要呼叫一個函數:

#define Z_OBJCE(zval) zend_get_class_entry(&(zval) TSRMLS_CC)

PHP7

到了PHP7,如我前面的文章深入理解PHP7核心之ZVAL所說, zval中直接儲存了zend_object物件的指標:

struct _zend_object {
    zend_refcounted_h gc;
    uint32_t          handle;
    zend_class_entry *ce;
    const zend_object_handlers *handlers;
    HashTable        *properties;
    zval              properties_table[1];
};

而EG(objects_store)也只是簡單的儲存了一個zend_object**等指標:

typedef struct _zend_objects_store {
    zend_object **object_buckets;
    uint32_t top;
    uint32_t size;
    int free_list_head;
} zend_objects_store;

而對於前面的COW的例子,對於IS_OBJECT來說, 用IS_TYPE_COPYABLE來區分,也就是,當發生COW的時候,如果這個型別沒有設定 IS_TYPE_COPYABLE,那麼就不會發生"複製".

#define IS_ARRAY_EX  (IS_ARRAY | ((IS_TYPE_REFCOUNTED | IS_TYPE_COLLECTABLE | IS_TYPE_COPYABLE) << Z_TYPE_FLAGS_SHIFT))
#define IS_OBJECT_EX (IS_OBJECT | ((IS_TYPE_REFCOUNTED | IS_TYPE_COLLECTABLE) << Z_TYPE_FLAGS_SHIFT))

如上,大家可以看到對於ARRAY來說定義了IS_TYPE_REFCOUNTED, IS_TYPE_COLLECTABLE和IS_TYPE_COPYABLE, 但是對於OBJECT, 則缺少了IS_TYPE_COPYABLE.

在SEPARATE_ZVAL中:

#define SEPARATE_ZVAL(zv) do {                          
        zval *_zv = (zv);                               
        if (Z_REFCOUNTED_P(_zv) ||                      
            Z_IMMUTABLE_P(_zv)) {                       
            if (Z_REFCOUNT_P(_zv) > 1) {                
                if (Z_COPYABLE_P(_zv) ||                
                    Z_IMMUTABLE_P(_zv)) {               
                    if (!Z_IMMUTABLE_P(_zv)) {          
                        Z_DELREF_P(_zv);                
                    }                                   
                    zval_copy_ctor_func(_zv);           
                } else if (Z_ISREF_P(_zv)) {            
                    Z_DELREF_P(_zv);                    
                    ZVAL_DUP(_zv, Z_REFVAL_P(_zv));     
                }                                       
            }                                           
        }                                               
    } while (0)

如果不是Z_COPYABLE_P, 那麼就不發生寫時分離。

這裡有的同學會問,那既然已經在zval中直接儲存了zend_object*了,那為啥還需要EG(objects_store)呢?

這裡有2個主要原因:

1. 我們需要在PHP請求結束的時候保證所有的物件的解構函式都被呼叫,因為object存在迴圈參照的情況,那如何快速的遍歷所有存活的物件呢? EG(objects_store)是一個很不錯的選擇。

2. 在PHPNG開發的時候,為了保證最大向後相容,我們還是需要保證獲取一個物件的handle的介面, 並且這個handle還是要保證原有的語意。

但實際上來說呢, 其實EG(objects_store)已經沒啥太大的用處了, 我們是可以在將來去掉它的。

好,接下來出現了另外一個問題,我們再看看zend_object的定義, 注意到末尾的properties_table[1], 也就是說,我們現在會把object的屬性跟物件一起分配記憶體。這樣做對快取友好。 但帶來一個變化就是, zend_object這個結構體現在是可能變長的。

那在當時寫PHPNG的時候就給我帶來了一個問題, 在PHP5時代,很多的自定義物件是這麼定義的(mysqli為例):

typedef struct _mysqli_object {
    zend_object         zo;
    void                *ptr;
    HashTable           *prop_handler;
} mysqli_object; /* extends zend_object */

也就是說zend_object都在自定義的內部類的頭部,這樣當然有一個好處是可以很方便的做cast, 但是因為目前zend_object變成變長了,並且更嚴重的是你並不知道使用者在PHP繼承了你這個類以後,他新增了多少屬性的定義。

於是沒有辦法,在寫PHPNG的時候,我做了大量的調整如下(體力活):

typedef struct _mysqli_object {
    void                *ptr;
    HashTable           *prop_handler;
    zend_object         zo;
} mysqli_object; /* extends zend_object */

也就是把zend_object從頭部,挪到了尾部,那為了可以從zend_object取得自定義物件,我們需要新增定義:

static inline mysqli_object *php_mysqli_fetch_object(zend_object *obj) {
    return (mysqli_object *)((char*)(obj) - XtOffsetOf(mysqli_object, zo));
}

這樣類似的程式碼大家應該可以在很多使用了自定義物件的擴充套件中看到。

這樣一來就規避了這個問題, 而在實際的分配自定義物件的時候,我們也需要採用如下的方法:

obj = ecalloc(1, sizeof(mysqli_object) + zend_object_properties_size(class_type));

這塊,大家在寫擴充套件的時候,如果用到自定義的類,一定要注意。

而之前在PHP5中的guard, 我們也知道並不是所有的類都會申明魔術方法,在PHP5中把guard放在object中會在大部分情況下都是浪費記憶體, 所以在PHP7中會,我們會根據一個類是否申明了魔術方法(IS_OBJ_HAS_GUARDS)來決定要不要分配,而具體的分配地方也放在了properties_table的末尾:

if (GC_FLAGS(zobj) & IS_OBJ_HAS_GUARDS) {
        guards = Z_PTR(zobj->properties_table[zobj->ce->default_properties_count]);
....
}

從而可以在大部分情況下,節省一個指標的記憶體分配。

最後就是, PHP7中在取一個物件的類的時候,就會非常方便了, 直接zvalu.value.obj->ce即可,一些類所自定的handler也就可以很便捷的存取到了, 效能提升明顯。

以上就是PHP7 核心 Object 深入理解的詳細內容,更多請關注TW511.COM其它相關文章!