Press "Enter" to skip to content

深入理解PHP7内核之OBJECT

前面的几篇,我系统的介绍了PHP7以后的ZVAL, Hashtable, 以及Reference, 今天我来讲讲Object(对象)的一些变化。

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也就可以很便捷的访问到了, 性能提升明显。

3 Comments

  1. minecraft classic
    minecraft classic September 22, 2020

    PHP7中在取一个对象的类的时候,就会非常方便了, 直接zvalu.value.obj->ce即可,一些类所自定的handler也就可以很便捷的访问到了

  2. hunter
    hunter August 19, 2020

    出现异常。
    Deprecated: The behavior of unparenthesized expressions containing both ‘.’ and ‘+’/’-‘ will change in PHP 8: ‘+’/’-‘ will take a higher precedence

    php7.4的这个异常 图和处理呢

  3. […] 这样的定义如果大家看过我之前的文章深入理解PHP7内核之Object应该不会陌生,当用户在PHP脚本端获取到Yaf_Request对象的时候,对应的就是yaf_request_object.std这个成员。 […]

Leave a Reply

Your email address will not be published. Required fields are marked *