PHP源码解析-变量与引用

前言
      《PHP7底层设计与源码实现》一书,内容不多也不少。看完后,对开发中的优化思路有不少启发,其中最多的,便是有关变量方面的认识,也是我个人认为,在日常开发中,最值得多注意多思考的方面。

        在此,个人结合github中最新的php-src,整理一些个人认为应该注意的点以及想法出来,与大家交流。同时,我会对其中的关键结构体在github上php-src源码中的定义的位置标注出来(截止发文时的最新版本)。这一点,是原书中没有标注的,在这里给出,更易于必要时查阅。

请确保具备以下知识基础
有C/C++基础
确保了解结构体struct和联合体union的定义和使用(特别是联合体的)
确保了解C/C++的取地址符&
正文
       很多同学应该都晓得php的写时拷贝这个点,但是我还是选择对这方面深入探讨,是因为我发现日常工作中,还是个人与其他同学理依然容易混淆php7与php5的拷贝机制,且网上大多数文章脱离了php源码仅对结论做列举和验证。因此,在这里,我结合github中的源码对该部分做知识梳理。

一个问题
我们先来做个赋值,并获取赋值前后的内存占用,以此抛出我们后续要讨论的问题。

$a = str_repeat('hello', 1);
$before = memory_get_usage();

$b = $a;
echo 'after: ' . (memory_get_usage() - $before); // 输出 after: 0


接下来,仅在第三行增加引用符号

$a = str_repeat('hello', 1);
$before = memory_get_usage();

$b = &$a; // 仅在这里增加取引用符
echo 'after: ' . (memory_get_usage() - $before); // 输出 after: 24


由此,我们可以得到一个简单的结论:取引用符不能帮助我们节省内存空间,反而会增加内存空间使用。学过C\C++的同学,可能会对此比较懵逼(实际上,我并不止一次听到用同学认为『&』与C/C++中的取地址符等效)

接下来,我们带着这点疑问,继续深入探讨,自然就明白了。

引用
先来看一段代码和原理图。

示例代码与原理图

$a = 10;
$b = $a;


实际上,在php7的底层中,存在一个zval结构体,任何的变量底层都是该结构体,且该结构体不存储真实value,其结构体中存在一个value字段,指向真正的存储值的结构体(在这里指向zend_string)。

因此,我们可以看到,php7底层已经做了优化。在赋值后,只会创建表示变量的zval结构体实例,不会创建新的zend_value结构体实例。

那么,什么时候会创建新的zend_value呢?这就是接下来要讲的写时拷贝。

简单的扩展:

memory_get_usage()仅跟踪底层中emalloc()分配的内存。
所有底层定义,我们都可以很方便的在php的github中的源码查阅,具体路径为php-src/Zend/zend_types.h。
变量的结构体是zval,实际上他是_zend_struct的宏定义(typedef)。
zval中,实际存储值的是结构体成员zend_value value(line 197 from zend_types.h),而zend_value是_zend_value的宏定义。
zend_value通过联合体实现支持存储多种类型的(line 176 from zend_types.h),其中实际存储字符串的是联合体成员zend_string。
zend_string是结构体_zend_string的宏定义,实际的字符串首地址即为_zend_string.val指向的首地址
写时拷贝
我们再来看一段代码与原理图

$a = str_repeat('hello1', 1);
$before = memory_get_usage();

$b = $a;
echo 'after: ' . (memory_get_usage() - $before) . PHP_EOL; // after: 0

$before = memory_get_usage();
$b = str_repeat('hello2', 1);
echo 'write after: ' . (memory_get_usage() - $before) . PHP_EOL; // write after: 40

通过对比上方两次赋值后的内存使用,可以看到,在第二次的时候,内存使用差值不再是0,因为,在执行$b = str_repeat('hello2', 1); 后,产生了一个新的zend_value,这就是所谓的写时拷贝——仅在变量值被执行写操作时,才拷贝新的zend_value空间,并对该新的zend_value执行相关修改操作。

这样的设计同时满足了两点需求(也是php7较php5的优点):

节省空间
避免对变量的其中一个引用做修改时,影响了另一变量的值,因为对其中一个变量做修改时,会拷贝新的空间。
需要注意的是,不仅对变量执行对某个新的动态值执行=赋值操作,-=、+=、*=等变量的写操作都会触发『写时拷贝』
取引用赋值
我们回顾一下,开篇的两段示例代码。

$a = str_repeat('hello', 1);
$before = memory_get_usage();

$b = $a;
echo 'after: ' . (memory_get_usage() - $before); // 输出 after: 0
$a = str_repeat('hello', 1);
$before = memory_get_usage();

$b = &$a; // 仅在这里增加取引用符
echo 'after: ' . (memory_get_usage() - $before); // 输出 after: 24


对于第二段代码,执行后实际上会产生如下效果。

       也就是说,产生了一个新的结构体zend_reference实例,那么,我们应该就能明白了,引用赋值后,多出来的空间就是来自这里的。
       当执行了引用赋值后,我们不应该将其理解为C/C++中的指向同一地址,而是二者会指向同一引用,而该引用的zval字段的value字段才指向了真正的值存储空间。在引用赋值后,之所以实现了修改其中一个变量,会影响到另一变量的效果,是因为当zend发现当前zval变量的类型zval.u1.type_flag是引用类型IS_REFERENCE时,就知道zval.value是引用类型,此时会继续搜索zval.value.zval,并对该zval类型字段执行相应修改。
       再简单点说就是,引用赋值后,通过将两个zval指向同一zend_reference实例,实现与真正的zend_value实例隔离,从而避免了写时拷贝带来的影响。

zend_reference是_zend_reference的宏定义(line 94 from zend_types.h)。
_zend_reference里面也有一个zval,此时,zend_reference.zval.value才存储了真正的值。
查看联合体_zend_value,我们可以发现,其中存在字段zend_reference *ref,这也是为什么变量底层的zval.value能存储zend_reference的原因。
到这里可能有点绕,建议浏览zend_types.h中,_zval_struct、_zend_value、_zend_reference三者的定义和结构,再结合上下文,应该就很好理解了。
小结
       现在,我们可以回到一开始的问题了:在函数传参或赋值过程中,使用取引用符号&,能减少内存空间占用吗?

       这是我在之前重构项目的过程中,为了功能模块解耦,出现同一数据,向下传递多层嵌套函数参数时,想到的一个问题,为了避免数据重复copy,考虑到这样是否能够减少运行时间和内存占用。然而,目前的情况来看,是不行的。取引用符号&,与C/C++中的取地址符含义不一样,并且,对于只读不写的参数,使用引用传递,反而更浪费内存。

垃圾回收
       又一个绕不开的,在这里,期望与最简单的描述,理清垃圾回收机制,而与童鞋们交流。在上文中,我们知道,当执行$b = $a时,不会创建新的的zend_value的存储空间,那么此时,除了会为b 创 建 一 个 z v a l , 并 将 z v a l . v a l u e 指 向 b创建一个zval,并将zval.value指向b创建一个zval,并将zval.value指向a的zval.value以外,还会对zval.value共同指向的zend_string.gc执行增加计数操作(gc实际上是一个结构体,其中有一个字段用于计数)。
       需要注意的是,php的整形和浮点型都直接存储在_zend_value中,并不会使用指针指向额外的空间,因此他们的引用计数由_zend_value.counted决定,它是zend_refcounted类型的指针,
而zend_refcounted其实是只有一个zend_refcounted_h类型字段gc的结构体。

       由此,我们可以知道,任何类型的变量都存在zend_refcounted_h类型的引用计数器,垃圾回收本质也正是对引用计数为0的zend_value执行回收。但是,它又不会马上回收,而是会先标记,当空间池满时,才会释放被标记的zend_value。

实际上,除了zend_string存在zend_refcounted_h类型的引用计数字段gc以外,_zend_array、_zend_object、_zend_resource等结构体都有(建议在zend_types.h文件中自行搜索关键字zend_refcounted_h)。
隐式的类型转换
       这点和前文所述内容关系不大,但是一个我认为非常重要的点,当整形数超出了最大值PHP_INT_MAX时,不会产生任何错误,而会自动转化为浮点型。

示例

$num = 0;
var_dump($num);        // 输出int(0)

$num = PHP_INT_MAX;
var_dump($num);        // 输出int(9223372036854775807)

$num++;
var_dump($num);        // 输出float(9.2233720368548E+18)


       然而,如果对float继续增加,会导致float不会有任何变化,进一步的,如果对变量赋值2*PHP_FLOAT_MAX,会导致变量溢出,但此时变量依然不会报错,而是会自动转化为INF(无穷大),同时,INF可比较,这一点可能会导致php在处理大量数据的时候,产生预料意外的情况。

$num = PHP_FLOAT_MAX;
var_dump($num);        // 输出float(1.7976931348623E+308)

$num = PHP_FLOAT_MAX + PHP_FLOAT_MAX / pow(2, 512);
var_dump($num);        // 输出float(1.7976931348623E+308)

$num = 2 * PHP_FLOAT_MAX;
var_dump($num);        // 输出float(INF)


       这个点,严格来讲,书中没有提及,是我在阅览过程中,无意中发现的一个点,我认为我们可能应该意识到并注意,避免可能对我们的程序产生意外的影响。当然,我们有时候也可以此来实现某些超出整形,但不超过浮点型的运算。

       关于浮点数运算,还有很多验证性实验可做,由于实际应用很难遇到,这里不一一列举,有兴趣的同学可以对PHP_FLOAT_MAX进行各种运算尝试。

在PHP的底层中,数值类型的存储只有zend_long和double两种形式。
zend_long的定义在php-src/Zend/zend_long.h中(line 31),它实际上是int64_t的类型宏定义typedef int64_t zend_long;。
总结
这里我们对上方描述的做个小总结。

函数参数传递过程中,使用『&』取引用赋值,在该参数永远不会被改变值的情况下,并不会减少内存空间的使用,反而会增加内存空间(zend_reference结构体)。
不能相信php的垃圾收机制,且要注意buffer不满时,垃圾回收机制不会立即释放空间。
循环引用不能被回收的原因,正是因为出现了自己对自己的引用,导致引用计数始终不为0,因此垃圾回收机制便始终不生效。
数值越界时,存在隐式的类型转换,int -> float -> INF,且INF减去任意值依然是INF,INF大于任何值,我们使用php做大数运算时必须注意这一点(比如他可能造成循环逻辑的死循环)。
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值