资讯专栏INFORMATION COLUMN

PHP7扩展开发(四):拷贝与引用

PingCAP / 825人阅读

摘要:如果,且仅仅是如果,引用计数变成了,引擎会销毁该。的结构重新定义了,都有一个同样的头用来存储引用计数拷贝引用有两种方法引用。需要使用指针的复杂类型比如字符串数组和对象会自己存储引用计数。

引用计数

迄今为止,我们向HashTables中加入的zval要么是新建的,要么是刚拷贝的。它们都是独立的,只占用自己的资源且只存在于某个HashTable中。作为一个语言设计的概念,创建和拷贝变量的方法是“很好”的,但是习惯了C程序设计就会知道,通过避免拷贝大块的数据(除非绝对必须)来节约内存和CPU时间并不少见。考虑这段用户代码:


如果执行zval_copy_ctor()(将会对字符串内容执行estrndup())将$a拷贝给$b,那么这个简短的脚本实际会用掉8M内存来存储同一4M文件的两份相同的副本。在最后一步取消$a只会更糟,因为原始字符串被efree()了。用C做这个将会很简单,大概是这样:b = a; a = NULL;。

Zend引擎的做法更聪明。当创建$a时,会创建一个潜在的string类型的zval,它含有日至文件的内容。这个zval通过调用zend_hash_add()被
赋给$a变量。当$a被拷贝给$b,引擎做类似下面的事情:

{
    zval **value;
    zend_hash_find(EG(active_symbol_table), "a", sizeof("a"), (void**)&value);
    ZVAL_ADDREF(*value);
    zend_hash_add(EG(active_symbol_table), "b", sizeof("b"),  value,sizeof(zval*));
}

实际代码会更复杂点,简单的说,就是通过引用计数来记录zval在符号表中、数组中、或其他地方被引用的次数。这样$b = $a赋值只要将其引用计数+1,而不用去进行内容拷贝。

当用户空间代码调用unset($a),引擎对该变量执行zval_ptr_dtor()。在前面用到的zval_ptr_dtor()中,你看不到的事实是,这个调用没有必要销毁该zval和它的内容。实际工作是减少refcount。如果,且仅仅是如果,引用计数变成了0,Zend引擎会销毁该zval。

有些简单数据类型不需要多带带分配内存,也不需要计数;PHP7中zval的long和double类型是 不需要 引用计数的。

php7的zval结构重新定义了,都有一个同样的头(zend_refcounted)用来存储引用计数:

typedef struct _zend_refcounted_h {
    uint32_t         refcount;          /* reference counter 32-bit */
    union {
        struct {
            ZEND_ENDIAN_LOHI_3(
                zend_uchar    type,
                zend_uchar    flags,    /* used for strings & objects */
                uint16_t      gc_info)  /* keeps GC root number (or 0) and color */
        } v;
        uint32_t type_info;
    } u;
} zend_refcounted_h;
拷贝 vs 引用

有两种方法引用zval。第一种,如上文示范的,被称为写复制引用(copy-on-write referencing)。第二种形式是完全引用(full referencing);当说起“引用”时,用户空间代码的编写者更熟悉这种, 以用户空间代码的形式出现类似于:$a = &$b;

在zval中,这两种类型的区别在于它的is_ref成员的值,0表示写复制引用,非0表示完全引用。注意,一个zval不可能同时具有两种引用类型。所以,如果变量起初是is_ref(即完全引用-译注),然后以拷贝的方式赋给新的变量,那么必将执行一个完全拷贝。考虑下面的用户空间代码:

 zend_array_1(refcount=1, value=[])
$b = &$a;      //$a, $b -> zend_reference_1(refcount=2) -> zend_array_1(refcount=1, value=[])
$c = $a;      //// $a, $b, $c -> zend_array_1(refcount=3, value=[])

在这段代码中,为$a创建并初始化了一个zval,将is_ref设为0,将refcount设为1。当$a被$b引用时,is_ref变为1,refcount递增至2。当拷贝至$c时,Zend引擎不能只是递增refcount至3,因为如此则$c变成了$a的完全引用。关闭is_ref也不行,因为如此会使$b看起来像是$a的一份拷贝而不是引用。所以此时分配了一个新的zval,并使用zval_copy_ctor()把原始(zval)的值拷贝给它。原始zval仍为is_ref==1、refcount==2,同时新zval则为is_ref=0、refcount=1。现在来看另一块内容相同的代码块,只是顺序稍有不同:

 zend_array_1(refcount=1, value=[])
$c = $a;  // $a, $c     -> zval_1(type=IS_ARRAY, refcount=2, is_ref=0) -> HashTable_1(value=[])
$b = &$a; // $c -> zval_1(type=IS_ARRAY, refcount=2, is_ref=0) -> HashTable_1(value=[])
          // $b, $a -> zval_1(type=IS_ARRAY, refcount=2, is_ref=1) -> HashTable_2(value=[])
          // $b 是 $a 的引用, 但却不是 $a 的 $c, 所以这里 zval 还是需要进行复制
          // 这样我们就有了两个 zval, 一个 is_ref 的值是 0, 一个 is_ref 的值是 1.

所有的变量都可以共享同一个数组,最终结果不变,$b是$a的完全引用,并且$c是$a的一份拷贝。然而这次的内部效果稍有区别。如前,开始时为$a创建一个is_ref==0并且refcount=1的新zval。$c = $a;语句将同一个zval赋给$c变量,同时将refcount增至2,is_ref仍是0。当Zend引擎遇到$b = &$a;,它想要只是将is_ref设为1,但是当然不行,因为那将影响到$c。所以改为创建新的zval并用zval_copy_ctor()将原始(zval)的内容拷贝给它。然后递减原始zval的refcount以表明$a不再使用该zval。代替地,(Zend)设置新zval的is_ref为1、refcount为2,并且更新$a和$b变量指向它(新zval)。

 zend_array_1(refcount=1, value=[])
 
$b = $a;   // $a = zval_1(type=IS_ARRAY) -> zend_array_1(refcount=2, value=[])
           // $b = zval_2(type=IS_ARRAY) ---^
 
// zval 分离在这里进行
$a[] = 1   // $a = zval_1(type=IS_ARRAY) -> zend_array_2(refcount=1, value=[1])
           // $b = zval_2(type=IS_ARRAY) -> zend_array_1(refcount=1, value=[])
 
unset($a); // $a = zval_1(type=IS_UNDEF),   zend_array_2 被销毁
           // $b = zval_2(type=IS_ARRAY) -> zend_array_1(refcount=1, value=[])

这个过程其实挺简单的。现在整数不再是共享的,变量直接就会分离成两个多带带的 zval,由于现在 zval 是内嵌的所以也不需要多带带分配内存,所以这里的注释中使用 = 来表示的而不是指针符号 ->,unset 时变量会被标记为 IS_UNDEF。

总结

PHP7 中最重要的改变就是 zval 不再多带带从堆上分配内存并且不自己存储引用计数。需要使用 zval 指针的复杂类型(比如字符串、数组和对象)会自己存储引用计数。这样就可以有更少的内存分配操作、更少的间接指针使用以及更少的内存分配。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/22093.html

相关文章

  • php7内核阅读(1)--数据容器zval和zend_value

    摘要:本文主要是针对,的话可以移步到庆哥的博客看,还有就是小菜我读的是内核剖析这本书。接下来我会使用到来调试源码本文有参照博客中的部分内容以及代码。 前言 工作+实习快一年了,搞php后端开发,一直很迷茫怎么提高自己,就先从php源码开始吧,本人比较菜,本文章写的比较赶时间,所以有什么错误或者漏掉的地方,望各位大神指正,多交流才能成长嘛,嘿嘿。本文主要是针对php7,php5的话可以移步到庆...

    canger 评论0 收藏0
  • 【天赢金创】PHP7Swoole

    摘要:但在密集计算方面比等静态编译语言差几十倍甚至上百倍。一使用栈内存在引擎和扩展中,经常要创建一个的变量,底层就是一个指针。代码中创建的变量也进行了优化,直接在栈内存上预分配。应用层与底层在错误抛出的方式全部统一为异常。 原文:http://rango.swoole.com/archives/440最近PHP官方终于发布了传说中的PHP7,虽然只是alpha版。PHP7号称是新一代的PHP...

    MingjunYang 评论0 收藏0
  • foreach遍历过程中的奇怪现象(PHP5)

    摘要:中基础中的三大坑,遍历,引用机制,数组。今天我们在讲讲中的一些奇怪现象。本文适合有一定基础的。运行流程共用一个结构体开始遍历数组,进行判断,拷贝数组是一个新的结构体,操作的是新的结构体。那么遍历数组时,全程与原数组无关。 PHP中基础中的三大坑,foreach遍历,引用机制&,数组。 今天我们在讲讲foreach中的一些奇怪现象。 在讲解之前,可以先看看我其他相关的文章,属于同一个大的...

    kgbook 评论0 收藏0
  • (PHP7内核剖析-11) 模块扩展

    摘要:编译工具这个脚本主要生成了编译需要的配置以及扩展的基本结构这个脚本主要是获取的安装信息用于生成文件编写扩展的基本步骤通过目录下脚本生成扩展的基本框架修改配置设置编译配置参数设置扩展的源文件依赖库函数检查等等定义一个这样的编译参数 1. 编译工具 (a).ext_skel:这个脚本主要生成了编译需要的配置以及扩展的基本结构 (b).php-config:这个脚本主要是获取PHP的安装信息...

    gplane 评论0 收藏0
  • PHP7扩展开发(五):回调php函数开发一个并行扩展

    摘要:起步很多时候,需要把控制权限交给用户,或者在扩展里完成某件事后去回调用户的方法。在扩展里是通过函数来调用用户空间的函数的。收集回调函数的返回值。回调函数需要传递参数的个数。利用语言多线程库来实现一个简单的并行扩展。 起步 很多时候,需要把控制权限交给用户,或者在扩展里完成某件事后去回调用户的方法。 在PHP扩展里是通过 call_user_function_ex 函数来调用用户空间的函...

    _ang 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<