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_Now.
##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 深入理解的詳細內容。更多資訊請關注PHP中文網其他相關文章!