您好,欢迎访问一九零五行业门户网

变量在 PHP7 内部的实现(一)_php实例

zval_1(type=is_long, value=42, refcount=3)// 下面几行是关于 zval 分离的$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 使用了循环回收的方法。当一个 zval 的计数减一时,就有可能属于循环的一部分,这时将 zval 写入到『根缓冲区』中。当缓冲区满时,潜在的循环会被打上标记并进行回收。
因为要支持循环回收,实际使用的 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,所以实际使用中只有一个指针是有用的。buffered 指针用于存储 zval 在根缓冲区的引用地址,所以如果在循环回收执行之前 zval 已经被销毁了,这个字段就可能被移除了。next 在回收销毁值的时候使用,这里不会深入。
修改动机
下面说说关于内存使用上的情况,这里说的都是指在 64 位的系统上。首先,由于 str 和 obj 占用的大小一样, zvalue_value 这个联合体占用 16 个字节(bytes)的内存。整个 zval 结构体占用的内存是 24 个字节(考虑到内存对齐),zval_gc_info 的大小是 32 个字节。综上,在堆(相对于栈)分配给 zval 的内存需要额外的 16 个字节,所以每个 zval 在不同的地方一共需要用到 48 个字节(要理解上面的计算方式需要注意每个指针在 64 位的系统上也需要占用 8 个字节)。
在这点上不管从什么方面去考虑都可以认为 zval 的这种设计效率是很低的。比如 zval 在存储整型的时候本身只需要 8 个字节,即使考虑到需要存一些附加信息以及内存对齐,额外 8 个字节应该也是足够的。
在存储整型时本来确实需要 16 个字节,但是实际上还有 16 个字节用于引用计数、16 个字节用于循环回收。所以说 zval 的内存分配和释放都是消耗很大的操作,我们有必要对其进行优化。
从这个角度思考:一个整型数据真的需要存储引用计数、循环回收的信息并且单独在堆上分配内存吗?答案是当然不,这种处理方式一点都不好。
这里总结一下 php5 中 zval 实现方式存在的主要问题:
zval 总是单独从堆中分配内存;
zval 总是存储引用计数和循环回收的信息,即使是整型这种可能并不需要此类信息的数据;
在使用对象或者资源时,直接引用会导致两次计数(原因会在下一部分讲);
某些间接访问需要一个更好的处理方式。比如现在访问存储在变量中的对象间接使用了四个指针(指针链的长度为四)。这个问题也放到下一部分讨论;
直接计数也就意味着数值只能在 zval 之间共享。如果想在 zval 和 hashtable key 之间共享一个字符串就不行(除非 hashtable key 也是 zval)。
php7 中的 zval
在 php7 中 zval 有了新的实现方式。最基础的变化就是 zval 需要的内存不再是单独从堆上分配,不再自己存储引用计数。复杂数据类型(比如字符串、数组和对象)的引用计数由其自身来存储。这种实现方式有以下好处:
简单数据类型不需要单独分配内存,也不需要计数;
不会再有两次计数的情况。在对象中,只有对象自身存储的计数是有效的;
由于现在计数由数值自身存储,所以也就可以和非 zval 结构的数据共享,比如 zval 和 hashtable key 之间;
间接访问需要的指针数减少了。
我们看看现在 zval 结构体的定义(现在在 zend_types.h 文件中):
struct _zval_struct { zend_value value; /* value */ union { struct { zend_endian_lohi_4( zend_uchar type, /* active type */ zend_uchar type_flags, zend_uchar const_flags, zend_uchar reserved) /* call info for ex(this) */ } 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;};
结构体的第一个元素没太大变化,仍然是一个 value 联合体。第二个成员是由一个表示类型信息的整型和一个包含四个字符变量的结构体组成的联合体(可以忽略 zend_endian_lohi_4 宏,它只是用来解决跨平台大小端问题的)。这个子结构中比较重要的部分是 type(和以前类似)和 type_flags,这个接下来会解释。
上面这个地方也有一点小问题:value 本来应该占 8 个字节,但是由于内存对齐,哪怕只增加一个字节,实际上也是占用 16 个字节(使用一个字节就意味着需要额外的 8 个字节)。但是显然我们并不需要 8 个字节来存储一个 type 字段,所以我们在 u1 的后面增加了了一个名为 u2 的联合体。默认情况下是用不到的,需要使用的时候可以用来存储 4 个字节的数据。这个联合体可以满足不同场景下的需求。
php7 中 value 的结构定义如下:
typedef union _zend_value { 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; zend_ast_ref *ast; zval *zv; void *ptr; zend_class_entry *ce; zend_function *func; struct { uint32_t w1; uint32_t w2; } ww;} zend_value;
首先需要注意的是现在 value 联合体需要的内存是 8 个字节而不是 16。它只会直接存储整型(lval)或者浮点型(dval)数据,其他情况下都是指针(上面提到过,指针占用 8 个字节,最下面的结构体由两个 4 字节的无符号整型组成)。上面所有的指针类型(除了特殊标记的)都有一个同样的头(zend_refcounted)用来存储引用计数:
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;
现在,这个结构体肯定会包含一个存储引用计数的字段。除此之外还有 type、flags 和 gc_info。type 存储的和 zval 中的 type 相同的内容,这样 gc 在不存储 zval 的情况下单独使用引用计数。flags 在不同的数据类型中有不同的用途,这个放到下一部分讲。
gc_info 和 php5 中的 buffered 作用相同,不过不再是位于根缓冲区的指针,而是一个索引数字。因为以前根缓冲区的大小是固定的(10000 个元素),所以使用一个 16 位(2 字节)的数字代替 64 位(8 字节)的指针足够了。gc_info 中同样包含一个『颜色』位用于回收时标记结点。
zval 内存管理
上文提到过 zval 需要的内存不再单独从堆上分配。但是显然总要有地方来存储它,所以会存在哪里呢?实际上大多时候它还是位于堆中(所以前文中提到的地方重点不是堆,而是单独分配),只不过是嵌入到其他的数据结构中的,比如 hashtable 和 bucket 现在就会直接有一个 zval 字段而不是指针。所以函数表编译变量和对象属性在存储时会是一个 zval 数组并得到一整块内存而不是散落在各处的 zval 指针。之前的 zval * 现在都变成了 zval。
之前当 zval 在一个新的地方使用时会复制一份 zval * 并增加一次引用计数。现在就直接复制 zval 的值(忽略 u2),某些情况下可能会增加其结构指针指向的引用计数(如果在进行计数)。
那么 php 怎么知道 zval 是否正在计数呢?不是所有的数据类型都能知道,因为有些类型(比如字符串或数组)并不是总需要进行引用计数。所以 type_info 字段就是用来记录 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< zend_array_1(refcount=2, value=[]) // $b = zval_2(type=is_array) ---^// zval 分离在这里进行$a[] = 1 // $a = zval_1(type=is_array) -> zend_array_2(refcount=1, value=[1]) // $b = zval_2(type=is_array) -> zend_array_1(refcount=1, value=[])unset($a); // $a = zval_1(type=is_undef), zend_array_2 被销毁 // $b = zval_2(type=is_array) -> zend_array_1(refcount=1, value=[])
这种情况下每个变量变量有一个单独的 zval,但是是指向同一个(有引用计数) zend_array 的结构体。修改其中一个数组的值时才会进行复制。这点和 php5 的情况类似。
类型(types)
我们大概看一下 php7 支持哪些类型(zval 使用的类型标记):
/* regular data types */#define is_undef 0#define is_null 1#define is_false 2#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/* internal types */#define is_indirect 15#define is_ptr 17
这个列表和 php5 使用的类似,不过增加了几项:
is_undef 用来标记之前为 null 的 zval 指针(和 is_null 并不冲突)。比如在上面的例子中使用 unset 注销变量;
is_bool 现在分割成了 is_false 和 is_true 两项。现在布尔类型的标记是直接记录到 type 中,这么做可以优化类型检查。不过这个变化对用户是透明的,还是只有一个『布尔』类型的数据(php 脚本中)。
php 引用不再使用 is_ref 来标记,而是使用 is_reference 类型。这个也要放到下一部分讲;
is_indirect 和 is_ptr 是特殊的内部标记。
实际上上面的列表中应该还存在两个 fake types,这里忽略了。
is_long 类型表示的是一个 zend_long 的值,而不是原生的 c 语言的 long 类型。原因是 windows 的 64 位系统(llp64)上的 long 类型只有 32 位的位深度。所以 php5 在 windows 上只能使用 32 位的数字。php7 允许你在 64 位的操作系统上使用 64 位的数字,即使是在 windows 上面也可以。
zend_refcounted 的内容会在下一部分讲。下面看看 php 引用的实现。
引用
php7 使用了和 php5 中完全不同的方法来处理 php & 符号引用的问题(这个改动也是 php7 开发过程中大量 bug 的根源)。我们先从 php5 中 php 引用的实现方式说起。
通常情况下, 写时复制原则意味着当你修改一个 zval 之前需要对其进行分离来保证始终修改的只是某一个 php 变量的值。这就是传值调用的含义。
但是使用 php 引用时这条规则就不适用了。如果一个 php 变量是 php 引用,就意味着你想要在将多个 php 变量指向同一个值。php5 中的 is_ref 标记就是用来注明一个 php 变量是不是 php 引用,在修改时需不需要进行分离的。比如:
zval_1(type=is_array, refcount=1, is_ref=0) -> hashtable_1(value=[])$b =& $a; // $a, $b -> zval_1(type=is_array, refcount=2, is_ref=1) -> hashtable_1(value=[])$b[] = 1; // $a = $b = zval_1(type=is_array, refcount=2, is_ref=1) -> hashtable_1(value=[1]) // 因为 is_ref 的值是 1, 所以 php 不会对 zval 进行分离
但是这个设计的一个很大的问题在于它无法在一个 php 引用变量和 php 非引用变量之间共享同一个值。比如下面这种情况:
zval_1(type=is_array, refcount=1, is_ref=0) -> hashtable_1(value=[])$b = $a; // $a, $b -> zval_1(type=is_array, refcount=2, is_ref=0) -> hashtable_1(value=[])$c = $b // $a, $b, $c -> zval_1(type=is_array, refcount=3, is_ref=0) -> hashtable_1(value=[])$d =& $c; // $a, $b -> zval_1(type=is_array, refcount=2, is_ref=0) -> hashtable_1(value=[]) // $c, $d -> zval_1(type=is_array, refcount=2, is_ref=1) -> hashtable_2(value=[]) // $d 是 $c 的引用, 但却不是 $a 的 $b, 所以这里 zval 还是需要进行复制 // 这样我们就有了两个 zval, 一个 is_ref 的值是 0, 一个 is_ref 的值是 1.$d[] = 1; // $a, $b -> zval_1(type=is_array, refcount=2, is_ref=0) -> hashtable_1(value=[]) // $c, $d -> zval_1(type=is_array, refcount=2, is_ref=1) -> hashtable_2(value=[1]) // 因为有两个分离了的 zval, $d[] = 1 的语句就不会修改 $a 和 $b 的值.
这种行为方式也导致在 php 中使用引用比普通的值要慢。比如下面这个例子:
zend_array_1(refcount=1, value=[])$b[] = 1; // $a, $b -> zend_reference_1(refcount=2) -> zend_array_1(refcount=1, value=[1])
上面的例子中进行引用传递时会创建一个 zend_reference,注意它的引用计数是 2(因为有两个变量在使用这个 php 引用)。但是值本身的引用计数是 1(因为 zend_reference 只是有一个指针指向它)。下面看看引用和非引用混合的情况:
zend_array_1(refcount=1, value=[])$b = $a; // $a, $b, -> zend_array_1(refcount=2, value=[])$c = $b // $a, $b, $c -> zend_array_1(refcount=3, value=[])$d =& $c; // $a, $b -> zend_array_1(refcount=3, value=[]) // $c, $d -> zend_reference_1(refcount=2) ---^ // 注意所有变量共享同一个 zend_array, 即使有的是 php 引用有的不是$d[] = 1; // $a, $b -> zend_array_1(refcount=2, value=[]) // $c, $d -> zend_reference_1(refcount=2) -> zend_array_2(refcount=1, value=[1]) // 只有在这时进行赋值的时候才会对 zend_array 进行赋值
这里和 php5 最大的不同就是所有的变量都可以共享同一个数组,即使有的是 php 引用有的不是。只有当其中某一部分被修改的时候才会对数组进行分离。这也意味着使用 count() 时即使给其传递一个很大的引用数组也是安全的,不会再进行复制。不过引用仍然会比普通的数值慢,因为存在需要为 zend_reference 结构体分配内存(间接)并且引擎本身处理这一块儿也不快的的原因。
结语
总结一下 php7 中最重要的改变就是 zval 不再单独从堆上分配内存并且不自己存储引用计数。需要使用 zval 指针的复杂类型(比如字符串、数组和对象)会自己存储引用计数。这样就可以有更少的内存分配操作、更少的间接指针使用以及更少的内存分配。
在下篇文章给大家介绍变量在 php7 内部的实现(二),感兴趣的朋友继续关注。
其它类似信息

推荐信息