深入PHP使用技巧之变量

总所周知,PHP与其他脚本语言一样属于弱变量类型的语言。同时PHP本身也是通过C语言来实现。本文主要介绍PHP内部是如何实现弱变量类型的,并且据此分析在PHP开发中的需要注意的一些使用技术。其中会重点分析PHP中的copy on write机制和引用相关方面的话题。本章节属于深入《深入PHP使用技巧》的第一部分。

如何实现弱变量

在了解PHP实现弱变量之前,可以先思考下:如何通过C/C++来实现弱变量类型的效果呢?
这个问题我再BIT培训课上基本上有两种答案:
方法1:采用C++的继承机制。首先定义一个基础类型

class Var 
{
};

然后基于Var,派生出不同的子类型IntVar/FloatVar/StringVar等等。
方法2:基于C语言的Struct。其中一个字段用于标识类型,另外一个字段用于存储数据,由于数据要是各种类型,所以通常需要采用指针,比如:

struct var {
    int type;
    void *data;
};

两种思路本身并没有太大区别,也都基本上能够满足需求。在PHP中采用了第二种思路,并且做了比较多的优化。在PHP中,所有变量都会对应同一种类型zval,其中zval也就是struct_zval_struct,具体定义如下:

typedef union _zvalue_value {
    long lval;                  /* long value */
    double dval;                /* double value */
    struct {
        char *val;
        int len;
    } str;
    HashTable *ht;              /* hash table value */
    zend_object_value obj;
} zvalue_value;
struct _zval_struct {
    /* Variable information */
    zvalue_value value;     /* value */
    zend_uint refcount;
    zend_uchar type;    /* active type */
    zend_uchar is_ref;
};

从zval可以看出,PHP在细节方面的确做了不少优化的功夫。
1 zend_uchar type。采用uchar节省内存。
2 zvalue_value value。采用union类替换void ,这样能节省空间,并且比void 更能表义清晰。
3 在字符串类型中,默认保留了字符串的长度。这样很容易做到字符串的二进制安全,并且在计算字符串长度的时候不需要进行扫描。

观察PHP弱变量的实现,也会有一下疑惑:
1 为什么没有int类型呢?其实在PHP中是由的,只是说默认int数据就保存在long中。
2 资源类型咋表现的呢?资源在PHP内部其实就是一数字。详细后续会介绍。
3 refcount和is_ref是干嘛的呢?

Reference counting & Copy-on-Write

PHP和其他语言类型,在其语法中有两种赋值方式:引用赋值和非引用赋值(普通的=赋值)。

<?php
    $a = 1;
    $b = $a; // 非引用赋值
    $c = &$a; // 引用赋值
?>

引用赋值和非引用赋值在PHP内部是如何实现的呢?一种通常的认识是:“引用赋值就是两个变量对应同一个zval,非引用赋值则是直接产生一个新的zval,同时把对应的值直接copy过来。”
也就是该代码的内存结构如下:
该图时大多人认为的PHP内存结构,是错误的
(该图时大多人认为的PHP内存结构,是错误的)
这样的确能够满足大部分情况下的需求,但显然不是最佳解决方案,尤其在内存管理上,比如说一下代码就会显得非常的低效。

<?php
    $arr = array(...); // 定义一个非常大的PHP数组
    myfunc($arr); // 每一个函数调用都试一次隐形的非引用赋值
?>

因为每次函数调用会进行一次内存dump,而大内存的内存dump非常耗CPU的。在C语言中一种解决方案是采用指针,所有函数调用尽量传递指针。的确很灵活高效,但也很难维护。指针可以说是C语言程序员心头的痛。还有一种更高级更有效的方法时采用引用计数(Reference counting)。

在PHP中,也可以采用引用来解决这样的问题,但你见过采用在PHP中大量使用引用的吗?显然很少。
在PHP内核中,Zval的实现正是采用了引用计数的概念,说起引用计数就不得不谈到copy-on-write 机制。这样前面谈到的refcount和is_ref就有作用了。

refcount:引用次数。在zval初始创建的时候就为1。每增加一个引用,则refcount ++。

is_ref:用于表示一个zval是否是引用状态。zval初始化的情况下会是0,表示不是引用。

在Zend/Zend.h内部有一些关于ZVAL的宏定义,里面比较清晰的解析了引用计数的一些规则,其中重点关注以下几个宏定义

#define INIT_PZVAL(z)       \\
    (z)->refcount = 1;       \\
    (z)->is_ref = 0;
#define SEPARATE_ZVAL_IF_NOT_REF(ppzv)      \\//非引用下的变量分离
    if (!PZVAL_IS_REF(*ppzv)) {             \\
        SEPARATE_ZVAL(ppzv);                \\
    }
#define SEPARATE_ZVAL_TO_MAKE_IS_REF(ppzv)  \\//非引用下的变量分离,并且设置引用
    if (!PZVAL_IS_REF(*ppzv)) {             \\
        SEPARATE_ZVAL(ppzv);                \\
        (*(ppzv))->is_ref = 1;               \\
    }
#define SEPARATE_ARG_IF_REF(varptr) \\     //引用下的变量分离
    if (PZVAL_IS_REF(varptr)) { \\
        zval *original_var = varptr; \\
        ALLOC_ZVAL(varptr); \\
        varptr->value = original_var->value; \\
        varptr->type = original_var->type; \\
        varptr->is_ref = 0; \\
        varptr->refcount = 1; \\
        zval_copy_ctor(varptr); \\
    } else { \\
        varptr->refcount++; \\
    }

这里面谈到两个重要的概念:

1、非引用下的变量分离。

非引用下的变量分离,是指在一堆非引用变量中插入引用的情况下,在PHP内部进行的一种内存操作。以下面的列子来看:

$a = 1;
$b = $a;
$c = &$b;

在前两句执行之后,内存结构如下图
这里写图片描述

在第三句 $c = &$b;语句中则会执行“非引用下的变量分离。”,具体步骤是:

将b分离出来,同时把a对应的zval的refcount-1。

copy 出一个新的zval,并把zval的is_ref设置成1.

把C指向这个新的zval,同时refcount ++

最终效果如下图:
这里写图片描述

2、引用下的变量分离。

引用下的变量分离,是指在一堆引用变量中进行一个非引用赋值操作,这个时候会直接执行copy内存的操作。

以下面的例子来说

$a = 1;
$b = &$a;
$c = $b;

在执行完前两行后,PHP中内存结构如下:
这里写图片描述
在第三句,则会执行“引用下的变量分离”也就是真正的copy,最终内存结构如下图
这里写图片描述
据此,基本上对PHP变量内部的一些原理比较清楚了,但还有一些需要注意点的:

1、PHP变量的引用计数特性,对于数组同样也存在。但注意,对于key则不生效。(具体在后面章节会分析到。)

2、PHP变量中的对象比较特殊,在PHP5之后,默认都是采用引用赋值的方式。具体实现可以参考Zend_objects.*系列代码。

3、对于分析PHP内部变量,推荐采用xdebug_debug_zval,而不要采用内置的debug_zval_dump。因为PHP内置的debug_zval_dump函数一方面无法处理is_ref,而且采用了引用的方式来处理,从而导致看到结果会有误解。

使用技巧结论

据此可以得出分析出不少结论:

1、在PHP开发中不推荐采用引用。因为PHP内部对内存优化本身做了不少工作,引用不会带来太多优化。(但注意推荐非强制)

2、在PHP中strlen是o(1)的。
转自:深入PHP使用技巧之变量

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值