变量在 PHP7 内部的实现(二)(4)

和 PHP5 相比,现在的实现中只有一个引用计数(zval 自身不计数),并且内存的使用量有了很大的缩减:40 个字节用于基础对象,每个属性需要 16 个字节,并且这还是算了 zval 之后的。间接访问的情况也有了显著的改善,因为现在中间层的结构体要么被去掉了,要么就是直接嵌入的,所以现在读取一个属性只有一层访问而不再是四层。

间接 zval

到现在我们已经基本提到过了所有正常的 zval 类型,但是也有一对特殊类型用于某些特定的情况的,其中之一就是 PHP7 新添加的 IS_INDIRECT 。

间接 zval 指的就是其真正的值是存储在其他地方的。注意这个 IS_REFERENCE 类型是不同的,间接 zval 是直接指向另外一个 zval 而不是像 zend_reference 结构体一样嵌入 zval。

为了理解在什么时候会出现这种情况,我们来看一下 PHP 中变量的实现(实际上对象属性的存储也是一样的情况)。

所有在编译过程中已知的变量都会被指定一个索引并且其值会被存在编译变量(CV)表的相应位置中。但是 PHP 也允许你动态的引用变量,不管是局部变量还是全局变量(比如 $GLOBALS ),只要出现这种情况,PHP 就会为脚本或者函数创建一个符号表,这其中包含了变量名和它们的值之间的映射关系。

但是问题在于:怎么样才能实现两个表的同时访问呢?我们需要在 CV 表中能够访问普通变量,也需要能在符号表中访问编译变量。在 PHP5 中 CV 表用了双重指针 zval** ,通常这些指针指向中间的 zval* 的表, zval* 最终指向的才是实际的 zval:

+------ CV_ptr_ptr[0] | +---- CV_ptr_ptr[1] | | +-- CV_ptr_ptr[2] | | | | | +-> CV_ptr[0] --> some zval | +---> CV_ptr[1] --> some zval +-----> CV_ptr[2] --> some zval

当需要使用符号表时存储 zval* 的中间表其实是没有用到的而 zval** 指针会被更新到 hashtable buckets 的响应位置中。我们假定有 $a 、 $b 和 $c 三个变量,下面是简单的示意图:

CV_ptr_ptr[0] --> SymbolTable["a"].pDataPtr --> some zval CV_ptr_ptr[1] --> SymbolTable["b"].pDataPtr --> some zval CV_ptr_ptr[2] --> SymbolTable["c"].pDataPtr --> some zval

但是 PHP7 的用法中已经没有这个问题了,因为 PHP7 中的 hashtable 大小发生变化时 hashtable bucket 就失效了。所以 PHP7 用了一个相反的策略:为了访问 CV 表中存储的变量,符号表中存储 INDIRECT 来指向 CV 表。CV 表在符号表的生命周期内不会重新分配,所以也就不会存在有无效指针的问题了。

所以加入你有一个函数并且在 CV 表中有 $a 、 $b 和 $c ,同时还有一个动态分配的变量 $d ,符号表的结构看起来大概就是这个样子:

SymbolTable["a"].value = INDIRECT --> CV[0] = LONG 42 SymbolTable["b"].value = INDIRECT --> CV[1] = DOUBLE 42.0 SymbolTable["c"].value = INDIRECT --> CV[2] = STRING --> zend_string("42") SymbolTable["d"].value = ARRAY --> zend_array([4, 2])

间接 zval 也可以是一个指向 IS_UNDEF 类型 zval 的指针,当 hashtable 没有和它关联的 key 时就会出现这种情况。所以当使用 unset($a) 将 CV[0] 的类型标记为 UNDEF 时就会判定符号表不存在键值为 a 的数据。

常量和 AST

还有两个需要说一下的在 PHP5 和 PHP7 中都存在的特殊类型 IS_CONSTANT 和 IS_CONSTANT_AST 。要了解他们我们还是先看以下的例子:

<?php function test($a = ANSWER, $b = ANSWER * ANSWER) { return $a + $b; } define('ANSWER', 42); var_dump(test()); // int(42 + 42 * 42)·

test() 函数的两个参数的默认值都是由常量 ANSWER 构成,但是函数声明时常量的值尚未定义。常量的具体值只有通过 define() 定义时才知道。

由于以上问题的存在,参数和属性的默认值、常量以及其他接受『静态表达式』的东西都支持『延时绑定』直到首次使用时。

常量(或者类的静态属性)这些需要『延时绑定』的数据就是最常需要用到 IS_CONSTANT 类型 zval 的地方。如果这个值是表达式,就会使用 IS_CONSTANT_AST 类型的 zval 指向表达式的抽象语法树(AST)。

到这里我们就结束了对 PHP7 中变量实现的分析。后面我可能还会写两篇文章来介绍一些虚拟机优化、新的命名约定以及一些编译器基础结构的优化的内容(这是作者原话)。

您可能感兴趣的文章:

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:http://www.heiqu.com/e427114415d54529eb7b68c0209d296a.html