在强类型的语言当中, 当使用一个变量之前, 我们需要先声明这个变量. 然而, 对于 PHP 来说, 在使用一个变量时, 我们不需要声明, 也不需要初始化, 直接对其赋值就可以使用, 这是如何实现的?
变量的声明和赋值
在 PHP 中没有对常规变量的声明操作, 如果要使用一个变量, 直接进行赋值操作即可. 在赋值操作的同时已经进行声明操作. 一个简单的赋值操作:
$a = 10;
使用 VLD 扩展查看其生成的中间代码为 ASSIGN. 依此, 我们找到其执行的函数为 ZEND_ASSIGN_SPEC_CV_CONST_HANDLER. (找到这个函数的方法之一:$a 为 CV,10 为 CONST, 操作为 ASSIGN. 其他方法可以参见 opcode 处理函数查找 http://www.php-internal.com/book/?p=chapt02/02-03-03-from-opcode-to-handler) CV 是 PHP 在 5.1 后增加的一个在编译期的缓存. 如我们在使用 VLD 查看上面的 PHP 代码生成的中间代码时会看到:
compiled vars: !0 = $a
这个 $a 变量就是 op_type 为 IS_CV 的变量.
IS_CV 值的设置是在语法解析时进行的.
参见 Zend/zend_complie.c 文件中的 zend_do_end_variable_parse 函数.
在这个函数中, 获取这个赋值操作的左值和右值的代码为:
- zval *value = &opline->op2.u.constant;
- zval **variable_ptr_ptr = _get_zval_ptr_ptr_cv(&opline->op1,
- EX(Ts), BP_VAR_W TSRMLS_CC);
由于右值为一个数值, 我们可以理解为一个常量, 则直接取操作数存储的 constant 字段, 关于这个字段的说明将在后面的虚拟机章节说明. 左值是通过 _get_zval_ptr_ptr_cv 函数获取 zval 值.
- static zend_always_inline zval **_get_zval_ptr_ptr_cv(const znode *node, const temp_variable *Ts, int type TSRMLS_DC)
- {
- zval ***ptr = &CV_OF(node->u.var);
- if (UNEXPECTED(*ptr == NULL)) {
- return _get_zval_cv_lookup(ptr, node->u.var, type TSRMLS_CC);
- }
- return *ptr;
- }
- // 函数中的 CV_OF 宏定义
- #define CV_OF(i) (EG(current_execute_data)->CVs[i])
_get_zval_ptr_ptr_cv 函数程序会先判断变量是否存在于 EX(CVs), 如果存在则直接返回, 否则调用_get_zval_cv_lookup, 通过 HastTable 操作在 EG(active_symbol_table) 表中查找变量. 虽然 HashTable 的查找操作已经比较快了, 但是与原始的数组操作相比还是不在一个数量级. 这就是 CV 类型变量的性能优化点所在. CV 以数组的方式缓存变量所在 HashTable 的值, 以取得对变量更快的访问速度.
如果变量不在 EX(CVs) 中, 程序会调用_get_zval_cv_lookup. 从而最后的调用顺序为: [_get_zval_ptr_ptr_cv] --> [_get_zval_cv_lookup] 在_get_zval_cv_lookup 函数中关键代码为:
- zend_hash_quick_find(EG(active_symbol_table), cv->name, cv->name_len+1,
- cv->hash_value, (void **)ptr)
这是一个 HashTable 的查找函数, 它的作用是从 EG(active_symbol_table) 中查找名称为 cv->name 的变量, 并将这个值赋值给 ptr. 最后, 这个在符号表中找到的值将传递给 ZEND_ASSIGN_SPEC_CV_CONST_HANDLER 函数的 variable_ptr_ptr 变量.
以上是获取左值和右值的过程, 在这步操作后将执行赋值操作的核心操作 -- 赋值. 赋值操作是通过调用 zend_assign_to_variable 函数实现. 在 zend_assign_to_variable 函数中, 赋值操作分为好几种情况来处理, 在程序中就是以几层的 if 语句体现.
情况一: 赋值的左值存在引用 (即 zval 变量中 is_ref__gc 字段不为 0), 并且左值不等于右值
这种情形描述起来比较抽象, 如下面的示例:
- $a = 10;
- $b = &$a;
- xdebug_debug_zval('a');
- $a = 20;
- xdebug_debug_zval('a');
试想, 如果我们来做这个 $b = &$a; 的底层实现, 我们可能会这样做:
判断左值是不是已经被引用过了;
左值已经被引用, 则不改变左值的引用计数, 将右值赋与左值;
事实上, ZE 也是用同样的方法来实现, 其代码如下:
- if (PZVAL_IS_REF(variable_ptr)) {
- if (variable_ptr!=value) {
- zend_uint refcount = Z_REFCOUNT_P(variable_ptr);
- garbage = *variable_ptr;
- *variable_ptr = *value;
- Z_SET_REFCOUNT_P(variable_ptr, refcount);
- Z_SET_ISREF_P(variable_ptr);
- if (!is_tmp_var) {
- zendi_zval_copy_ctor(*variable_ptr);
- }
- zendi_zval_dtor(garbage);
- return variable_ptr;
- }
- }
PZVAL_IS_REF(variable_ptr) 判断 is_ref__gc 字段是否为 0. 在左值不等于右值的情况下执行操作. 所有指向这个 zval 容器的变量的值都变成了 * value. 并且引用计数的值不变. 下面是这种情况的一个示例:
上面的例子的输出结果:
- a:
- (refcount=2, is_ref=1),int 10
- a:
- (refcount=2, is_ref=1),int 20
情况二: 赋值的左值不存在引用, 左值的引用计数为 1, 左值等于右值
在这种情况下, 应该是什么都不会发生吗? 看一个示例:
$a = 10; $a = $a;
看上去真的像是什么都没有发生, 左值的引用计数还是 1, 值仍是 10 . 然而在这个赋值过程中,$a 的引用计数经历了一次加一和一次减一的操作. 如以下代码:
- if (Z_DELREF_P(variable_ptr)==0) { // 引用计数减一操作
- if (!is_tmp_var) {
- if (variable_ptr==value) {
- Z_ADDREF_P(variable_ptr); // 引用计数加一操作
- }
- ...// 省略
情况三: 赋值的左值不存在引用, 左值的引用计数为 1, 右值存在引用
用一个 PHP 的示例来描述一下这种情况:
- $a = 10;
- $b = &$a;
- $c = $a;
这里的 $c = $a; 的操作就是我们所示的第三种情况. 对于这种情况, ZEND 内核直接创建一个新的 zval 容器, 左值的值为右值, 并且左值的引用计数为 1. 也就是说, 这种情形 $c 不会与 $a 指向同一个 zval. 其内核实现代码如下:
- garbage = *variable_ptr;
- *variable_ptr = *value;
- INIT_PZVAL(variable_ptr); // 初始化一个新的 zval 变量容器
- zval_copy_ctor(variable_ptr);
- zendi_zval_dtor(garbage);
- return variable_ptr;
在这个例子中, 若将 $c = $a; 换成 $c = &$a;,$a,$b 和 $c 三个变量的引用计数会发生什么变化?
将 $b = &$a; 换成 $b = $a; 呢?
大家可以将答案回复在下面:)
情况四: 赋值的左值不存在引用, 左值的引用计数为 1, 右值不存在引用
这种情形如下面的例子:
$a = 10; $c = $a;
这时, 右值的引用计数加上, 一般情况下, 会对左值进行垃圾收集操作, 将其移入垃圾缓冲池. 垃圾缓冲池的功能是在 PHP5.3 后才有的. 在 PHP 内核中的代码体现为:
- Z_ADDREF_P(value); // 引用计数加 1
- *variable_ptr_ptr = value;
- if (variable_ptr != &EG(uninitialized_zval)) {
- GC_REMOVE_ZVAL_FROM_BUFFER(variable_ptr); // 调用垃圾收集机制
- zval_dtor(variable_ptr);
- efree(variable_ptr); // 释放变量内存空间
- }
- return value;
情况五: 赋值的左值不存在引用, 左值的引用计数为大于 0, 右值存在引用, 并且引用计数大于 0
一个演示这种情况的 PHP 示例:
- $a = 10;
- $b = $a;
- $va = 20;
- $vb = &$va;
- $a = $va;
最后一个操作就是我们的情况五. 使用 xdebug 看引用计数发现, 最终 $a 变量的引用计数为 1,$va 变量的引用计数为 2, 并且 $va 存在引用. 从源码层分析这个原因:
- ALLOC_ZVAL(variable_ptr); // 分配新的 zval 容器
- *variable_ptr_ptr = variable_ptr;
- *variable_ptr = *value;
- zval_copy_ctor(variable_ptr);
- Z_SET_REFCOUNT_P(variable_ptr, 1); // 设置引用计数为 1
从代码可以看出是新分配了一个 zval 容器, 并设置了引用计数为 1, 印证了我们之前的例子 $a 变量的结果.
除上述五种情况之外, zend_assign_to_variable 函数还对全部的临时变量做了处理. 变量赋值的各种操作全部由此函数完成.
变量的销毁
在 PHP 中销毁变量最常用的方法是使用 unset 函数. unset 函数并不是一个真正意义上的函数, 它是一种语言结构. 在使用此函数时, 它会根据变量的不同触发不同的操作.
一个简洁的例子:
- $a = 10;
- unset($a);
使用 VLD 扩展查看其生成的中间代码:
- compiled vars: !0 = $a
- line # * op fetch ext return operands
- ---------------------------------------------------------------------------------
- 2 0> EXT_STMT
- 1 ASSIGN !0, 10
- 3 2 EXT_STMT
- 3 UNSET_VAR !0
- 4> RETURN 1
去掉关于赋值的中间代码, 得到 unset 函数生成的中间代码为 UNSET_VAR, 由于我们 unset 的是一个变量, 在 Zend/zend_vm_execute.h 文件中查找到其最终调用的执行中间代码的函数为: ZEND_UNSET_VAR_SPEC_CV_HANDLER 关键代码代码如下:
- target_symbol_table = zend_get_target_symbol_table(opline, EX(Ts),
- BP_VAR_IS, varname TSRMLS_CC);
- if (zend_hash_quick_del(target_symbol_table, varname->value.str.val,
- varname->value.str.len+1, hash_value) == SUCCESS) { // 删除 HashTable 元素
- zend_execute_data *ex = execute_data;
- do {
- int i;
- if (ex->op_array) {
- for (i =
来源: http://www.taocms.org/667.html