摘要:翻译自 http://nikic.github.io/2015/05/05/Internal-value-representation-in-PHP-7-part-1.html 介绍了PHP7的内部实现。
上一篇文章讲了PHP7中对hashtable的实现的改进。这篇将探索PHP Value的新的描述方式。
因为篇幅的原因,分成两部分:上篇来描述 zval (Zend value) 的实现在PHP 5 和 PHP 7 的不同,包括对引用(references)的实现。下篇将详述不同数据类型的细节,比如字符串、对象等。
PHP5中的Zvals
在PHP5中,zval的数据结构是这样定义的:
typedef struct _zval_struct {
zvalue_value value;
zend_uint refcount__gc;
zend_uchar type;
zend_uchar is_ref__gc;
} zval;
一个zval包含了一个值(value),一个类型(type),以及一些其它的 __gc 附加信息。value是一个联合体(union),以便存储各种不同类型的数据:
typedef union _zvalue_value {
long lval; // 存储布尔值,整数,以及资源
double dval; // 存储浮点数
struct { // 存储字符串
char *val;
int len;
} str;
HashTable *ht; // 存储数组
zend_object_value obj; // 存储对象
zend_ast *ast; // 存储常量表达式
} zvalue_value;
C语言中的联合体(union)每次只能保存一个成员数据,联合体的长度等于各成员中最长的长度。所有的成员数据被存储在同一个内存地址,在访问特定成员时,数据类型会被转换。比如你读取联合体的lval,联合体的值将会被解释成一个有符号的整数。如果你读取dval成员,联合体的值就会被当作double类型。依此类推。
为了正确的读取联合体成员,zval中使用type来标记联合体中正在被使用的成员类型,type是一组定义好的整数:
#define IS_NULL 0 /* Doesn't use value */
#define IS_LONG 1 /* Uses lval */
#define IS_DOUBLE 2 /* Uses dval */
#define IS_BOOL 3 /* Uses lval with values 0 and 1 */
#define IS_ARRAY 4 /* Uses ht */
#define IS_OBJECT 5 /* Uses obj */
#define IS_STRING 6 /* Uses str */
#define IS_RESOURCE 7 /* Uses lval, which is the resource ID */
/* Special types used for late-binding of constants */
#define IS_CONSTANT 8
#define IS_CONSTANT_AST 9
PHP5中的引用计数
PHP5的zval存在堆中,PHP需要跟踪这些zval是否正在被使用,或者应该被释放掉。为了达到这个目的,使用了引用计数:refcount__gc 记录了一个zval当前被“引用”的次数。例如:$a = $b = 42 ,42这个值被两个变量引用,所以她的refcount是2。当refcount到0的时候,表示这个值没有用了,可以被释放掉了。
注意这里提到的“引用”(值被使用的次数)和PHP中的引用(&)是两回事。文章将使用“引用”和“PHP引用”来区别这两个概念。现在提到的都是“引用”,所以请先忽略“PHP引用”
另外一个和引用相关的概念叫做“写时复制(copy on write)”:一个zval如果没有被修改的话,可以被多个使用者共享使用。需要改变这个值时,才需要复制(分离)一次,然后修改这个复制出来的值。
看一个关于写时复制和zval销毁的例子:
$a = 42; // $a -> zval_1(type=IS_LONG, value=42, refcount=1)
$b = $a; // $a, $b -> zval_1(type=IS_LONG, value=42, refcount=2)
$c = $b; // $a, $b, $c -> zval_1(type=IS_LONG, value=42, refcount=3)
// The following line causes a zval separation
$a += 1; // $b, $c -> zval_1(type=IS_LONG, value=42, refcount=2)
// $a -> zval_2(type=IS_LONG, value=43, refcount=1)
unset($b); // $c -> zval_1(type=IS_LONG, value=42, refcount=1)
// $a -> zval_2(type=IS_LONG, value=43, refcount=1)
unset($c); // zval_1 is destroyed, because refcount=0
// $a -> zval_2(type=IS_LONG, value=43, refcount=1)
引用计数有一个致命的缺陷:循环引用。为了解决循环引用,PHP加入了一个循环收集器(cycle collector)。zval的引用计数递减,并且可能成为环上一个部分的时候,zval会被写入到“root buffer”。一旦root buffer满了的时候,可能的环会被标记并被GC收集清理。
为了支持这个额外的环收集器, 实际使用的zval结构,是这样的:
typedef struct _zval_gc_info {
zval z;
union {
gc_root_buffer *buffered;
struct _zval_gc_info *next;
} u;
} zval_gc_info;
zval_gc_info 结构体嵌入了一个zval,还有一个附加的指针——u,联合体,所以u也是一个可以有两个指针类型的指针。buffered指针用来存储在root buffer中zval的位置,这样可以在环清理之前,把已经销毁的zval从环中移除。next用于环清理的时候。
改变的动机
先来看一下大小(64位系统下):zvalue_value有16字节大,因为str和obj需要这么大空间。整个zval结构有24个字节大小(包括对齐),zval_gc_info 有32字节。而且,这些堆上分配时,还需要额外的16字节。这样我们需要为每个zval分配48个字节的空间。
这样我们开始思考zval的实现是否效率低下。想一下简单的存储一个整形,数据需要8字节。类型标记无论如何都要需要,他只有一个字节,但是需要8个字节对齐。
这16个字节是我们“需要”的,我们还需要另外16个字节处理引用计数和循环引用收集,还有另外16字节分配开销。这还不算我们真正在内存分配和释放时操作的开销,这些开销也很大。
问题来了:简单的整数,是否真多需要存储引用计数、循环引用收集、堆分配值?当然不用!
这里罗列了一些PHP5中zval实现上的问题:
- zval总是在堆上分配空间
- zval总是引用计数,总是保留处理循环引用的收集器的信息,即便没什么意义。
- 直接引用计数zvals,在object和resource上,实际上是重复计数了。(背后的原因,下一篇再说)
- 一些情况下,会产生大量的间接寻址。比如访问一个在变量中的对象,会需要四层指针引用。下一章会详细说明。
- 直接zval引用计数,也已意味着这些值只能在zval之间共享。例如:一个字符串就不能既用于zval又用于hashtable的键值。(除非,hashtable的键值也用zval)
PHP7中的zval
来看PHP7中的zval实现。最根本的改变,是zval不再独自在堆上分配内存,而且不在zval结构中存储引用计数了。 那些指针指向的复杂的值(像字符串,数组或者对象)他们自身就可以保存引用计数。(Instead any complex values they may point to (like strings, arrays or objects) will store the refcount themselves. ?)这有以下优点:
+ 简单类型的值,不再需要请求分配内存,不使用引用计数。 + 不会再有重复的引用计数,在object中,只有object内部的引用计数。 + 因为现在的引用计数保存在值自身中,所以这些值可以被不同zval结构体分别共享。一个字符串,即可用在zval中,也可以用在hashtable的key中。 + 少了很多间接寻址。意味着,在你需要寻找值时使用的指针变少了。
现在新的zval是这样定义的:
struct _zval_struct {
zend_value value;
union {
struct {
ZEND_ENDIAN_LOHI_4(
zend_uchar type,
zend_uchar type_flags,
zend_uchar const_flags,
zend_uchar reserved)
} v;
uint32_t type_info;
} u1;
union {
uint32_t var_flags;
uint32_t next; // hash collision chain
uint32_t cache_slot; // literal cache slot
uint32_t lineno; // line number (for ast nodes)
uint32_t num_args; // arguments number for EX(This)
uint32_t fe_pos; // foreach position
uint32_t fe_iter_idx; // foreach iterator index
} u2;
};
第一个成员和PHP5中的基本一致,zend_value也是一个value联合体。第二个成员是一个整形村粗的类型信息,使一个联合体进一步分为单独的字节(你可以先忽略ZEND_ENDIAN_LOHI_4宏定义,这个是用来真对不同平台的处理字节序的)。成员结构中最重要的两个子成员:type 和 type_flags,type和以前的差不多,type_flags我稍后会说到。
这里存在小问题:value成员是8字节大小,由于结构体会被填充,所以即便只是再增加一个字节,也会扩大到16个字节。我们明显不需要8个字节存储type,这就是为什么会有另外一个联合体u2,默认情况下是不使用的,但是可以设定为存储4个字节的数据。不同的成员对应这个数据槽不同的用法。
value联合体,在PHP7中,看起来有一些小不同:
typedef union _zend_value {
zend_long lval;
double dval;
zend_refcounted *counted;
zend_string *str;
zend_array *arr;
zend_object *obj;
zend_resource *res;
zend_reference *ref;
zend_ast_ref *ast;
// Ignore these for now, they are special
zval *zv;
void *ptr;
zend_class_entry *ce;
zend_function *func;
struct {
ZEND_ENDIAN_LOHI(
uint32_t w1,
uint32_t w2)
} ww;
} zend_value;
首先,记住现在的zval是8字节的,而非16字节的。他只能直接存储整型(lval)和双精度浮点数(dval),其它所有的一切,都是指针。所有的指针类型(标记special那行注释的上面)使用引用计数,相同的头定义在zend_refconted 中:
struct _zend_refcounted {
uint32_t refcount;
union {
struct {
ZEND_ENDIAN_LOHI_3(
zend_uchar type,
zend_uchar flags,
uint16_t gc_info)
} v;
uint32_t type_info;
} u;
};
这个结构体中包含引用计数refcount。另外还包括类型type,一些标记flags,和gc信息gc_info。type是zval的type的复制来的,允许GC不用保存zval就可以区别不同的引用计数结构体类型。flags有不同的使用目的,下一章会按照类型分别介绍。
gc_info 相当于旧zval中的buffered。但是没有存储指针到root buffer中,而是包含了一个它的索引。因为root buffer已经扩充大小(10000元素)足够使用16位整数,替代一个64位指针。gc_info还编码了节点的“颜色”,用来标记回收中的节点。
Zval内存管理 前面已经提到了,zval不在单独的使用堆空间。但是显然需要存储空间,他说怎么办到的?当zval是堆上分配存储的结构体的一部分的时候,他们直接嵌入结构体中。例如,一个hashtable桶将直接把zval加入到自己的结构中,而不是保存一个指针,再指向一个独立的zval。函数的编译的变量表或对象的属性表,将分配一个区块,而不是存储指针指向单独的zval。现在zval存储通常会减少一级间接寻址。以前是 zval * 现在是 zval。
当zval在新的地方被使用的时候,之前是复制zval* ,增加它的引用计数。现在,复制zval的内容,可能增加值指向的引用计数,如果值使用了引用计数(?)
但是PHP如何知道一个value是不是使用了引用计数?他不能仅仅通过type来判断,因为一些类型比如字符串和数字,并不是总是使用引用计数的。zval中的type_info里面有一个bit,用来标记zval是不是引用计数。这里有其它的按位编码的类型属性:
#define IS_TYPE_CONSTANT (1<<0) /* special */
#define IS_TYPE_IMMUTABLE (1<<1) /* special */
#define IS_TYPE_REFCOUNTED (1<<2)
#define IS_TYPE_COLLECTABLE (1<<3)
#define IS_TYPE_COPYABLE (1<<4)
#define IS_TYPE_SYMBOLTABLE (1<<5) /* special */
三个重要的属性, “refcounted(引用计数)”, “collectable(可收集)” and “copyable(可复制)”。我们已经说过引用计数的意义。collectable