php5的变量实现
php通过一个zval结构体来实现变量,对于全局变量,php维护一个全局的hashtable,通过某种散列关系将变量名和对应的zval指针保存起来,这个hashtable称为symbol_table。对于函数,会建立一个局部的hashtable存储局部变量。php中的非对象/资源变量在传参时传递的是值,但实际上也是传递的指针,将局部hashtable中相应的变量映射到全局hashtable中的相应指针中,这是为了减少内存和时间开销采用了写时拷贝机制。
写时拷贝(copy on write):在需要复制一个资源时,并不总是直接复制值,而是首先选择复制指针,当两个指向同一资源的两个变量中的一个需要发生改变时再进行值复制。在很多时候,资源并不会被改变,采用COW机制可以减少很多不必要的时间和空间开销。Linux中的fork函数就采用的这种机制。
与之对应的机制称为写时改变(change on write):当值发生改变时,直接改变值如$b=&$a。
那么php如何分辨一个zval是copy on write/change on write。如果一个zval是copy on write的,如b,那么这个zval的refcount=2。refcount>2表示当前zval被多于1个的变量引用,那么某一个变量对改zval值进行修改时,需要首先将该zval拷贝一份,并将拷贝的副本分配给需要修改的变量再进行修改,这个过程称为分离。如果一个zval是change on write的,如$a=&$b,那么php会将该zval的is_ref__gc置1标志该zval成为一个引用,对该zval指进行修改时不会进行分离。那么一个变量即存在cpoy又存在change怎么处理呢,如果对一个copy的zval进行change on write处理时,判断到refcount>1,那么会首先对该zval进行分离再进行change on write处理。如果对一个change的zval进行copy on write处理,那么直接拷贝当前zval。可以看下面的代码
//php5.6
<?php
$a = '1';
$b = $a;//copy on write
debug_zval_dump($a);//输出string(1) "1" refcount(3),有三个变量指向当前zval,因为写时拷贝机制,$a,$b和传进debug_zval_dump()的参数都是指向了当前zval
$c = $a;
debug_zval_dump($a);//输出string(1) "1" refcount(4)
$d = &$a;
debug_zval_dump($a);//输出string(1) "1" refcount(1),对一个copy变量$a进行change on write处理,首先进行分离。那么$a和$d已经是一对change on write的变量了,此时再传入debug_zval_dump()函数,函数传参时也是值传递即copy on write,对一个change的zval进行copy on write处理,会直接拷贝zval,因此传入的参数是一个独立的变量所有refcount=1。
php5中的zval结构体_zval_struct实现
struct _zval_struct {
union {
long lval;
double dval;
struct {
char *val;//字节型指针8字节
int len;//整型4字节
} str;
HashTable *ht;
zend_object_value obj;
zend_ast *ast;
} value;//value变量通过一个联合体储存变量的值或指针
zend_uint refcount__gc;//计数,指向当前值的变量数,在debug_zval_dump()函数中输出的refcount
zend_uchar type;//变量类型标志位
zend_uchar is_ref__gc;//引用标志位,用于标志当前值是否是引用如$b=&$a
};
在这个_zval_struct结构体中,最主要的部分是一个value联合体,这个联合体储存了变量的值或指针,变量的类型由zend_uchar类型的type确定。在value联合体中最大的部分是str结构体占用12字节内存,在内存对齐的情况下,value共占用16字节内存。剩下的部分包括一个4个字节的zend_uint类型的refcount__gc和两个单字节zend_uchar类型的type和is_ref__gc,在内存对齐的情况下zval结构体共占用24字节内存,并且没有预留下拓展的空间。在5.3以后的版本中,为了解决数组和字符串变量的循环引用问题,使用了一个新的zval_gc_info结构体来扩充zval,这个结构体占8字节。这样在php5中,一个变量会至少占用32字节内存。并且由于zval结构和cow机制造成很大的空间和时间开销,如以下代码:
//php5.6
$a = '1';
$b = &$a;
print_r($a);
这里传入print_r()函数的参数本可以利用cow机制避免不必要的开销,但是由于$a在第二行和$b进行了change on write绑定,使$a成为了一个引用,那么在函数传递参数时,由于字符串是通过值传递即copy on write的,那么对一个change的zval进行copy on write时必须要进行分离,即使函数内并没有对该变量进行修改。如果这个函数在一个循环里重复运行几千几万次,就会造成很大的无效开销。
php7中的变量实现
struct _zval_struct {
union {
zend_long lval; //long value
double dval; //double value
zend_refcounted *counted;
zend_string *str;
zend_array *arr;
zend_object *obj;
zend_resource *res;
zend_reference *ref; //refenerce
zend_ast_ref *ast; //abstract syntax tree
zval *zv;
void *ptr;
zend_class_entry *ce;
zend_function *func;
struct {
uint32_t w1;
uint32_t w2;
} ww;
} 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是一个联合体,所以实际上type_info的值就是上面ZEND_ENDIAN_LOHI_4结构体的值,通过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;
};
php7的zval通过一个value联合体保存变量,一个u1联合体记录变量类型,u2联合体为各种辅助变量。value8字节,u1和u2分别为4字节,共16字节,相比php5节省了一半的内存。
value中可以保存一个指针或者一个long/double。在php7中引用独立出来成为一种类型的变量。u1中的type类型定义如下
/* regular data types */
#define IS_UNDEF 0 //标记为未定义,如unset后的变量
#define IS_NULL 1
#define IS_FALSE 2 //对于布尔类型,直接在type中储存了指IS_FALSE/IS_TRUE
#define IS_TRUE 3
#define IS_LONG 4
#define IS_DOUBLE 5
#define IS_STRING 6
#define IS_ARRAY 7
#define IS_OBJECT 8
#define IS_RESOURCE 9
#define IS_REFERENCE 10
/* constant expressions */
#define IS_CONSTANT 11
#define IS_CONSTANT_AST 12
/* fake types */
#define _IS_BOOL 13
#define IS_CALLABLE 14
#define IS_ITERABLE 19
#define IS_VOID 18
/* internal types */
#define IS_INDIRECT 15
#define IS_PTR 17
#define IS_ERROR 20
通过type值取出相应的value值。如果是IS_LONG/IS_DOUBLE则直接取出value值作为zval值,若是IS_FLASE/IS_TRUE则直接把type作为zval值,否则获取相应指针获取值。php7中的long和double不再使用COW机制。
u1中的type_flags用于标记变量的一些属性,8位的type_flags中每一位都可以表示一个标志,最多可以同时有8个标志如下
//zval,作用于zval也就是zval.u1.v.type_flags
IS_TYPE_CONSTANT //是常量类型
IS_TYPE_IMMUTABLE //不可变的类型, 比如存在共享内存的数组
IS_TYPE_REFCOUNTED //需要引用计数的类型
IS_TYPE_COLLECTABLE //可能包含循环引用的类型(IS_ARRAY, IS_OBJECT)
IS_TYPE_COPYABLE //可被复制的类型, 如对象和资源就不是
IS_TYPE_SYMBOLTABLE //zval保存的是全局符号表
//以下作用与具体变量的gc.u.v.flags
//string
IS_STR_PERSISTENT //是malloc分配内存的字符串
IS_STR_INTERNED //INTERNED STRING
IS_STR_PERMANENT //不可变的字符串, 用作哨兵作用
IS_STR_CONSTANT //代表常量的字符串
IS_STR_CONSTANT_UNQUALIFIED //带有可能命名空间的常量字符串
//array
IS_ARRAY_IMMUTABLE //同IS_TYPE_IMMUTABLE
//object
IS_OBJ_APPLY_COUNT //递归保护
IS_OBJ_DESTRUCTOR_CALLED //析构函数已经调用
IS_OBJ_FREE_CALLED //清理函数已经调用
IS_OBJ_USE_GUARDS //魔术方法递归保护
IS_OBJ_HAS_GUARDS //是否有魔术方法递归保护标志
zend_refcounted_h,在需要进行引用计数的数据类型结构体中都包含这个结构体,gc通过这个结构体来实现垃圾回收,gc可以不关心数据的类型。
typedef struct _zend_refcounted_h {
uint32_t refcount; /* reference counter 32-bit */
union {
struct {
ZEND_ENDIAN_LOHI_3(
zend_uchar type,
zend_uchar flags, /* used for strings & objects */
uint16_t gc_info) /* keeps GC root number (or 0) and color */
} v;
uint32_t type_info;
} u;
} zend_refcounted_h;
字符串
struct _zend_string{
zend_refcounted_h gc; //一个gc结构体,储存了gc相关的信息,使gc进行垃圾回收时可以不关心数据类型
zend_ulong h; //字符串哈希值,避免数组访问时重复计算哈希值,当字符串被当作数组索引时才计算该值
sizt_t len;
char val[1]; //柔性数组
}
对于一个字符串结构体,我们可以直接用一个char指针来储存字符串,但是这样在访问字符串时需要两次访问内存,一次是读地址,一次是读数据。并且在释放资源时也要先释放字符串指针再释放结构体。在C99标准中加入了一种柔性数组的结构,可以使php中的字符串操作更高效
那么如何改进呢?很容易想到,我们将字符串值和结构体存储在一片连续的内存空间就可以了。这样的话,访问字符串与释放字符串的内存空间,均仅需1次内存访问。
鉴于这种代码结构所产生的重要作用,C99甚至把它收入了标准中。C99使用不完整类型实现柔性数组成员,在C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组(flexible array)成员(也叫伸缩性数组成员),但结构中的柔性数组成员前面必须至少一个其他成员。柔性数组成员允许结构中包含一个大小可变的数组。柔性数组成员只作为一个符号地址存在,而且必须是结构体的最后一个成员,sizeof 返回的这种结构大小不包括柔性数组的内存。柔性数组成员不仅可以用于字符数组,还可以是元素为其它类型的数组。包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
- 第一个问题:为什么要存长度len?不存长度,直接和C语言一样通过字符串的'\0'来判断字符串结束不行吗?不行。这里有一个二进制安全的问题。
- 二进制安全:写入的数据和读出来的数据完全相同,就是二进制安全的。
- 假设你写入了一个字符串的内容为:hello\0world,按照C语言的读取字符串的方法就会判定\0是字符串结束的标志,读出来就是hello,这样读出来的数据就和写入的数据不一致,就是非二进制安全的。
- 如果存了长度,就不会管你是否有\0,从头开始读字符串,一直读len长度为止即可。
- 第二个问题:最后一个字段改成char val[0]可以吗?可以。写成char val[1]是出于可移植性的考虑。有些编译器不支持[0]数组,可将其改成[]或[1]均可。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(char * args[]){
typedef struct s {
int a;
char b[0];
}s;
s * a = (s*)(malloc(sizeof(s)+4*sizeof(char)));
a->a = 4;
strcpy(a->b, "he");
}
(gdb) b main
Breakpoint 1 at 0x1149: file string.c, line 5.
(gdb) run
Starting program: /home/whye/Desktop/php/phpsrc/str
Breakpoint 1, main (args=0x555555555060 <_start>) at string.c:5
5 int main(char * args[]){
(gdb)
(gdb) n
10 s * a = (s*)(malloc(sizeof(s)+4*sizeof(char)));
(gdb)
11 a->a = 4;
(gdb)
12 strcpy(a->b, "he");
(gdb)
14 }
(gdb) p a
$1 = (s *) 0x5555555592a0
(gdb) p *a
$2 = {a = 4, b = 0x5555555592a4 "he"}
(gdb) p sizeof(s)
$3 = 4
(gdb) p sizeof(a)
$4 = 8
(gdb) p a+1
$5 = (s *) 0x5555555592a4
(gdb) p (char*)(a+1)
$6 = 0x5555555592a4 "he"
引用实现
struct _zend_reference {
zend_refcounted_h gc;
zval val;
}
在php7中,引用也成为了一种数据类型,对于以下代码
<?php
$a = 'hello';
$b = $a;
$c = &$a;
上面的_zend_reference和_zend_string实例中gc计数均为2。
参考
深入理解PHP原理之变量(Variables inside PHP)
深入理解PHP原理之变量分离/引用(Variables Separation)