惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

大猫的无限游戏
大猫的无限游戏
Webroot Blog
Webroot Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
T
Threat Research - Cisco Blogs
V2EX - 技术
V2EX - 技术
L
LINUX DO - 热门话题
Google DeepMind News
Google DeepMind News
Recorded Future
Recorded Future
S
Schneier on Security
I
InfoQ
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
The GitHub Blog
The GitHub Blog
S
Security @ Cisco Blogs
O
OpenAI News
W
WeLiveSecurity
Vercel News
Vercel News
阮一峰的网络日志
阮一峰的网络日志
Simon Willison's Weblog
Simon Willison's Weblog
人人都是产品经理
人人都是产品经理
Cloudbric
Cloudbric
The Last Watchdog
The Last Watchdog
The Hacker News
The Hacker News
Google Online Security Blog
Google Online Security Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
GbyAI
GbyAI
NISL@THU
NISL@THU
T
Tailwind CSS Blog
V
Visual Studio Blog
PCI Perspectives
PCI Perspectives
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Jina AI
Jina AI
D
DataBreaches.Net
B
Blog RSS Feed
N
News and Events Feed by Topic
N
News and Events Feed by Topic
H
Heimdal Security Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
腾讯CDC
Latest news
Latest news
V
Vulnerabilities – Threatpost
Hacker News: Ask HN
Hacker News: Ask HN
WordPress大学
WordPress大学
V
V2EX
aimingoo的专栏
aimingoo的专栏
博客园 - 司徒正美
Apple Machine Learning Research
Apple Machine Learning Research
D
Darknet – Hacking Tools, Hacker News & Cyber Security
The Register - Security
The Register - Security
Help Net Security
Help Net Security

Comments for 风雪之隅

关于调用约定(cdecl、fastcall、stcall、thiscall) 的一点知识 - 风雪之隅 PHP 8新特性之Attributes(注解) - 风雪之隅 博客迁移到腾讯云 - 风雪之隅 提升PHP性能之改变Zend引擎分发方式 - 风雪之隅 如何调试PHP的Core之获取基本信息 - 风雪之隅 请手动释放你的资源(Please release resources manually) - 风雪之隅 在Qcon 2015 北京上的演讲PPT - PHP7 Linux上配置Nginx+PHP5(FastCGI) - 风雪之隅 在PHP Module中获取$_GET/$_POST/$_COOKIE的方法研究 - 风雪之隅 深入理解Javascript之this关键字 - 风雪之隅 Yar-2.1 新功能介绍 - 风雪之隅 Yaf and Phalcon, which is faster? PHP CLI模式下的多进程应用 - 风雪之隅 PHP5.2.*防止Hash冲突拒绝服务攻击的Patch - 风雪之隅 PHP中的Hash算法 - 风雪之隅 PHP单引号和双引号的区别 - 风雪之隅 深入理解PHP之数组(遍历顺序) - 风雪之隅 深入理解PHP原理之变量分离/引用(Variables Separation) - 风雪之隅 PHP Taint - 一个用来检测XSS/SQL/Shell注入漏洞的扩展 - 风雪之隅 BTwitter(Twitter In Bash) - 风雪之隅 automake,autoconf使用详解 - 风雪之隅 HTTPOXY漏洞说明 - 风雪之隅 Nginx(PHP/fastcgi)的PATH_INFO问题 - 风雪之隅 关于PHP浮点数你应该知道的(All 'bogus' about the float in PHP) 使用PHP Embed SAPI实现Opcodes查看器 - 风雪之隅 一个关于Zend O+的小分享 - 风雪之隅 深入理解PHP原理之对象(一) - 风雪之隅 在PHP中使用协程实现多任务调度 - 风雪之隅 一些PHP Coding Tips[2011/04/02最后更新] - 风雪之隅 Curl的毫秒超时的一个"Bug" - 风雪之隅 Javascript作用域原理 - 风雪之隅 深入理解PHP原理之Opcodes - 风雪之隅 深入理解Zend SAPIs(Zend SAPI Internals) - 风雪之隅 Nginx + PHP CGI的一个可能的安全漏洞 - 风雪之隅 PHP 8新特性之JIT简介 - 风雪之隅 PHP FFI详解 - 一种全新的PHP扩展方式 - 风雪之隅 令人困惑的strtotime - 风雪之隅 PHP的性能演进(从PHP5.0到PHP7.1的性能全评测) - 风雪之隅 让PHP7达到最高性能的几个Tips - 风雪之隅
深入理解PHP7内核之OBJECT - 风雪之隅
wlb · 2023-10-23 · via Comments for 风雪之隅

前面的几篇,我系统的介绍了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也就可以很便捷的访问到了, 性能提升明显。