在php7变量的实现(1)

< PHP
$数组=范围(0, 1000000);
$ = $数组;
var_dump(count($array)); / /这里将你们分离


由于很多详细的描述,本文将分为两个部分:第一部分主要介绍了变量之间的差异(Zend值)在PHP5和php7实施,并参考执行。第二部分分析单一类型的细节(字符串对象)。

在PHP5 zval

在PHP5中zval结构定义如下:


_zval_struct { typedef struct
zvalue_value价值;
zend_uint refcount__gc;
zend_uchar型;
zend_uchar is_ref__gc;
zval };


如上所述,变量包含一个字段值,类型,和两__gc后缀。价值是一个用于存储不同类型的价值联盟:


_zvalue_value { typedef联盟
长的语句; / /布尔型、整型和资源类型
双dval; / /浮点型
结构字符串
字符*;
Int len;
STR };
哈希表* HT; / /阵列
zend_object_value obj; / /对象
zend_ast * AST; / /常量表达式(php5.6只)
zvalue_value };


C语言结合的特征是只有一个成员是有效的在一个时间,和内存分配比赛谁最需要记忆的成员,所有成员都是存储在内存中的同一位置,不同的值存储的需要。当你需要存储语句,符号塑造,当你需要双,它存储的双精度浮点数。

必须指出,联合体中当前存储的数据类型被记录到类型字段中,并用整数标记。

#定义is_null 0不使用价值 / * * /

#定义is_long 1使用语句 / * * /

#定义is_double 2采用双 / * * /

#定义is_bool 3值0和语句使用 / * * / 1

#定义is_array 4使用HT / * * /

#定义is_object 5使用OBJ / * * /

#定义is_string 6使用STR / * * /

#定义is_resource 7采用的是 /语句,资源ID

用于后期绑定的特殊类型

#定义is_constant 8

#定义is_constant_ast 9

PHP5中的引用计数

在PHP5中,机制的内存是从堆(heap)分别分配。(少数例外),PHP需要知道哪些变量是使用什么需要被释放,所以我们需要使用引用计数:对refcount__gc在zval的值是用来保存一些变量本身被引用,例如,$ = B = 42, 42美元是被两个变量,所以它的引用计数为2。如果引用计数变为0,这意味着变量的使用,和记忆可以被释放。

注意,这里提到的引用计数不是PHP代码(使用)中的引用,而是使用变量的次数。后两个需要同时使用PHP参考和引用来区分两个概念。在这里,PHP的一部分被忽略了。

引用计数和密切相关的复制概念上写:多参考,zaval唯一共享的情况是没有变化,一旦你需要改变机制的价值参考,复制(分离)的一个变量,然后修改复制机制。

这里的一个例子,写时复制和机制的破坏。


< PHP
美元= 42; / /美元-> zval_1(类型= is_long,值= 42,引用计数= 1)
$ = $ / /;$,$,zval_1(类型= is_long,值= 42,引用计数= 2)
$c = $; / / $,$,$ C,zval_1(类型= is_long,值= 42,引用计数= 3)

/ /下面是关于zval分离
美元= 1; / / $,$ C,zval_1(类型= is_long,值= 42,引用计数= 2)
/ /美元-> zval_2(类型= is_long,值= 43,引用计数= 1)

unset($); / / $ C -> zval_1(类型= is_long,值= 42,引用计数= 1)
/ /美元-> zval_2(类型= is_long,值= 43,引用计数= 1)

unset($ C); / / zval_1被毁,因为引用计数= 0
/ /美元-> zval_2(类型= is_long,值= 43,引用计数= 1)


引用计数有一个致命的问题:无法检查和释放循环引用(使用内存)。为了解决这一问题,PHP使用的回收方法。当一个变量的数量减少,这可能是属于循环的一部分,当变量写入根缓冲。当缓冲区满,潜在的周期是明显和再生。

因为支持循环,实际使用的zval结构如下:


_zval_gc_info { typedef struct
变量z;
联盟{
gc_root_buffer *缓冲;
结构_zval_gc_info *下;
u };
zval_gc_info };


一个正常的zval结构嵌入在zval_gc_info结构,并增加了两个指针参数,但它属于同一联盟,美国因此在实际使用中只有一个指针,指针是用来存储缓冲机制的参考地址在根缓冲,所以如果变量被回收之前,该领域可能被删除。其次是用回收的破坏价值,也不会去深。

修改的动机

以下是关于内存的使用,指的是64位的系统。首先,由于STR和对象的大小,这zvalue_value财团占用16字节的内存(字节)。整个zval结构的内存是24字节(考虑内存对齐),对zval_gc_info的大小是32字节。综上所述,在堆(相对于堆栈)分配给变量的内存需要额外的16个字节,所以在不同的地方,每一个变量需要总共使用48字节(计算方式理解以上需要注意每个指针在64位系统也需要8个字节)。

在这一点上,机制设计的效率是很低的,不管它是什么考虑。例如,当变量存储整数,它只需要8个字节本身。即使考虑额外的信息和内存对齐,额外的8字节应该是足够的。

用于存储整数需要实际上是16个字节,但实际上有16字节和16字节为引用计数回收。因此,内存的分配和释放机制是一个很好的运行,这是我们优化是必要的。

从这个角度考虑:一个整数真的需要存储引用计数、循环信息和堆上分配内存吗答案当然不是。这不是一笔好交易。

这是存在于机制实现PHP5中存在的主要问题的总结:

zval总是独自从堆内存;

Zval always stores information about reference counting and recycling, even if it is an integer that may not require such information;

当使用一个对象或一个资源时,一个直接引用将导致两个计数(原因在下一部分);

一些间接访问需要更好的处理方式。例如,对存储在变量中的对象的访问现在由四个指针间接使用(指针链的长度为四)。

直接计数意味着价值只能共享机制。如果你想分享机制和哈希表的键之间的字符串,你可以不做(除非哈希表的关键是机制)。

在php7 zval

在php7,机制有了一个新的方式实现的。最根本的变化是由变量所需的存储不再是从堆中分配和分别,引用计数不再存储本身。复杂数据类型的引用计数,如字符串、数组和对象,存储本身这种方法有以下好处:

简单的数据类型不需要单独分配内存,也不需要计数。

在对象中,只有对象自身存储的计数是有效的。

由于计数现在的数量本身存储,也可以和非zval结构共享,如变量和哈希表的键;

减少间接访问所需的指针数。



让我们看看在zval结构定义了(现在在zend_types。h文件):


结构_zval_struct {
zend_value值; / * * /
联盟{
struct {
zend_endian_lohi_4(
zend_uchar型、主动型 / * * /
zend_uchar type_flags,
zend_uchar const_flags,
zend_uchar预留电话信息(Ex) / * * /本)
V };
uint32_t type_info;
U1 };
联盟{
uint32_t var_flags;
uint32_t下哈希碰撞链; / * * /
uint32_t cache_slot文字缓存槽; / * * /
uint32_t株系(AST值; / * * /节点)
uint32_t num_args;参数个数为(前/本)。
uint32_t fe_pos foreach位置; / * * /
uint32_t fe_iter_idx foreach迭代器指数; / * * /
U2 };
};




结构的第一个元素变化不大。第二个成员是由表示信息类型的类型和包含四个字符变量的结构组成的一个联合体。它可以忽略zend_endian_lohi_4宏,它只是用来解决跨平台的尺寸和最终的问题。此子的更重要的部分是型(之前)和type_flags,要解释下。

也有在上述地方的一个小问题:价值应该占8个字节,但由于内存对齐,即使只有一个字节,它实际上占用16字节(用一个字节表示8个额外的字节),但显然我们不需要8个字节来存储类型字段,所以我们添加一个联盟称为U1 U2后面。它不是默认的,可以用来存储4个字节的数据,当你需要使用它。这个联盟能满足不同应用场景的需求。

在php7价值结构定义如下:


_zend_value { typedef联盟
zend_long lval长值; / * * /
双双双值; / * * /
zend_refcounted *数;
zend_string * STR;
zend_array * ARR;
zend_object * OBJ;
zend_resource * RES;
zend_reference *参考;
zend_ast_ref * AST;
zval * ZV;
void*指针;
zend_class_entry *;
zend_function *功能;
struct {
uint32_t W1;
uint32_t W2;
WW };
zend_value };


首先要注意的是,通过价值联盟所需要的内存是8字节,而不是16。它只存储语句或双数据直接。在其他情况下,它是指针。如上所述,指针占用8个字节,和底部的结构是由两个4字节无符号整数。以上所有指针类型(除特殊标记)有相同的头(zend_refcounted)用来存放引用计数:


_zend_refcounted_h { typedef struct
uint32_t引用计数参考计数器位; / * * /
联盟{
struct {
zend_endian_lohi_3(
zend_uchar型,
zend_uchar旗帜,用于字符串对象 / * * /
uint16_t gc_info保持GC根(数) / *或* 0)和颜色
V };
uint32_t type_info;
u };
zend_refcounted_h };


现在,这种结构将包含一个字段存储的引用计数。除此之外,还有类型、旗帜和gc_info.type店相同的内容类型在zval,GC使用引用计数没有存储机制。国旗在不同数据类型的不同用途,这是下一部分。

gc_info作为缓冲PHP5中的相同,但它已不再是一个指向根缓冲区,而是一个指数,因为根缓冲区的大小是固定的(10000元),所以16位(2字节)的数字而不是64位(8字节)的gc_info指针是不够的。还包含一个标签节点的颜色位时,用于回收。

内存管理机制

正如上面提到的,需要的内存分配机制不再是分别从堆。但很显然,总有一个地方存放它,所以它会在哪里存在呢事实上,大部分的时间是在堆(所以以上在本地密钥不堆,但一提到,只分布在其它嵌入式)的数据结构,如哈希表和桶,现在将有一个机制的领域而不是指针,函数表编译变量和对象属性将存储时有了一个总体的内存块,而不是变量指针四处散落的变量数组。以往zval *现在已经成为zval。

在zval是用来在一个新的地方,复制机制*复制和增加引用计数。你现在可以复制变量的值直接(忽略U2),在某些情况下,它可能会增加,结构指针指向的参考点的数量(如果是数的)。

那么PHP知道变量计算呢不是所有的数据类型可以是已知的,因为有些类型,如字符串或数组,不总是需要引用计数,所以type_info字段用于记录是否变量数,和这个字段的值是在以下情况:


#定义is_type_constant(1<<0)特殊 / * * /
#define IS_TYPE_IMMUTABLE (1<<1) special / * * /
#定义is_type_refcounted(1<<2)
#定义is_type_collectable(1<<3)
#定义is_type_copyable(1<<4)
#define IS_TYPE_SYMBOLTABLE (1<<5) special / * * /


注:在7.0.0正式版,这段宏定义宏为zval.u1.v.type_flags.this应使用注释的错误因为上述领域的zend_uchar型。

type_info的三个主要属性是引用计数的计数问题已经提到。可回收是用来标记机制是否参与循环,而不是字符串通常是可数的,但你不能创建的字符串循环引用。

是否可以重复用于指示是否需要创建复制时(原复制是用来表示,它可能无法在中国得到很好的理解),一个相同的实体。复制属于深复制,例如,当复制的数组,它不仅简单地增加数组的引用计数,但创建新的值的数组。但有些类型,如对象和资源,甚至复制只能增加引用计数,属于不可复制的类型。这也符合目标和资源的现有的语义(同样是php7,不只是PHP5)。

下表显示了不同类型使用的标记(X标记都是特性)。简单类型是指不使用结构指针的整数类型或布尔类型。下表也有一个不可变的标记,用于标记一个不可变的数组,下一部分将进一步详细说明。

将字符串(保留字符)没有被提到过,事实上,它是一个函数名,变量名,等等不计数,不可重复的字符串。

引用计数可复制| | | |永恒收藏

---------------- + + + + ---------- ---------- ------------- ------------

简单类型| | | |

字符串X | | | X |

将字符串| | | |

阵列X X X | | | |

不可变的数组| | | | X

对象x x | | | |

资源| | | |

参考X | | | |

要理解这一点,我们可以看看几个例子来更好的理解内存管理的工作机制。

以下是一个整数的行为模式,这是简化在PHP5的例子的基础上,在上一篇文章。


< PHP
美元= 42; / /美元= zval_1(类型= is_long,值= 42)
$ = $ / /美元;= zval_1(类型= is_long,值= 42)
/ / $ B = zval_2(类型= is_long,值= 42)
美元= 1; / /美元= zval_1(类型= is_long,值= 43)
/ / $ B = zval_2(类型= is_long,值= 42)
撤消(合一); / /美元= zval_1(类型= is_undef)
/ / $ B = zval_2(类型= is_long,值= 42)


过程很简单,它不再是一个共享的整数,分离变量将直接为两个独立的变量,因为变量是目前嵌入式系统并不需要一个单独的内存分配,所以在这里评论使用,而不是一个指针=符号>,设置变量将被标记为is_undef。这是一个更复杂的情况下:


< PHP
美元= {}; / /美元= zval_1(类型= is_array)- zend_array_1(引用计数为1,价值= { })
$ = $ / /美元;= zval_1(类型= is_array)- zend_array_1(引用计数为2,价值= { })
/ / $ B = zval_2(类型= is_array)--- ^
从这里 / /机制
{ } = 1美元/美元= zval_1(类型= is_array)- zend_array_2(引用计数为1,价值= { 1 })
/ / $ B = zval_2(类型= is_array)- zend_array_1(引用计数为1,价值= { })
撤消(合一); / /美元= zval_1(类型= is_undef),该zend_array_2破坏
/ / $ B = zval_2(类型= is_array)- zend_array_1(引用计数为1,价值= { })


在这种情况下,每个变量有一个变量,但它是一个指向相同的(引用计数)zend_array。复制了一份当一个数组的值被修改。这是一个类似的PHP5。

类型(类型)

让我们看一看什么类型的php7支持(用zval类型标签):


常规数据类型
#定义is_undef 0
#定义is_null 1
#定义is_false 2
#define IS_TRUE 3
#定义is_long 4
#定义is_double 5
#定义is_string 6
#定义is_array 7
#定义is_object 8
#定义is_resource 9
#定义is_reference 10
常量表达式
#定义is_constant 11
#定义is_constant_ast 12
内部类型
#定义is_indirect 15
#定义is_ptr 17


这个列表是类似于使用PHP5,但是它增加了几项:

is_undef用来标记变量指针,以前是空的(没有冲突的is_null)。例如,取消注销变量是使用上面的例子。

is_bool现在分为两项,is_false和is_true.the斑纹布尔类型现在是直接记录到类型,优化了类型检查。然而,这种变化对用户是透明的,或者只有一个布尔类型的数据(在PHP脚本)。

PHP引用不再注明is_ref,但使用is_reference型。这也将在下一部分。

is_indirect和is_ptr特殊的内部标记。

事实上,上面的列表中应该有两个假类型,这是被忽略的。

的is_long型代表一个zend_long价值而非本土语言的长型。原因是在Windows 64位系统的长型(LLP64)只有32位深度。所以PHP5只能使用32位数字windows.php7允许你使用64位数字的64位操作系统,甚至在Windows。

zend_refcounted的内容将在下一部分讨论。让我们在PHP参考的实施看。

引用

php7采用完全不同的方式处理问题,PHP符号引用PHP5(这种变化也不php7发展过程的根源)。让我们在PHP5 PHP参考开始执行。

通常,写作时间复制原则意味着你需要单独在zval确保PHP变量的值总是修改修改。这是电话的意思。

但这个规则不适用时,PHP参考使用。如果一个PHP变量是一个PHP参考,这意味着你想点多个PHP变量相同的值。在PHP5的is_ref标签是用来标明一个PHP变量是一个PHP的参考,而不需要分离的时候这是修改。例如:


< PHP
美元= {}; / /美元-> zval_1(类型= is_array,引用计数= 1,is_ref = 0)- hashtable_1(值= { })
$ = $ / /;$,$,zval_1(类型= is_array,引用计数= 2,is_ref = 1)- hashtable_1(值= { })

$ { } = 1; / /美元= $ = zval_1(类型= is_array,引用计数= 2,is_ref = 1)- hashtable_1(值= { 1 })
/ /因为1是is_ref价值,所以PHP不会从zval分离






但是这个设计的一个大问题是它不能在PHP引用变量和PHP非引用变量之间共享相同的值:


< PHP
美元= {}; / /美元-> zval_1(类型= is_array,引用计数= 1,is_ref = 0)- hashtable_1(值= { })
$ = $ / /;$,$,zval_1(类型= is_array,引用计数= 2,is_ref = 0)- hashtable_1(值= { })
C = B /美元美元美元美元美元,B,C,zval_1(类型= is_array,引用计数= 3,is_ref = 0)- hashtable_1(值= { })
美元D = $ C; / / $,$,zval_1(类型= is_array,引用计数= 2,is_ref = 0)- hashtable_1(值= { })
/ / $ C美元D > zval_1(类型= is_array,引用计数= 2,is_ref = 1)- hashtable_2(值= { })
/ /美元D是参考美元的C,而不是美元的B,所以机制还需要复制到这里
所以我们有两个变量,一个is_ref值0,值1 is_ref。
美元D { } = 1; / / $,$,zval_1(类型= is_array,引用计数= 2,is_ref = 0)- hashtable_1(值= { })
/ / $ C美元D > zval_1(类型= is_array,引用计数= 2,is_ref = 1)- hashtable_2(值= { 1 })
/ /因为有两个独立的变量,$ { } = 1声明不会修改美元美元和B的值。


这种行为还导致PHP中引用的速度比普通值慢:


< PHP
$数组=范围(0, 1000000);
$ = $数组;
var_dump(count($array)); / /这里将你们分离


因为计数()只接受值传递调用,但是数组是一个PHP引用,计数()在执行前实际上有一个完整的数组复制。

现在让我们看看在php7 PHP参考实施看看。因为zval不再distributies个人的记忆,它将不能够使用相同的实现在PHP5的。所以添加is_reference型,和zend_reference用于存储参考值:


结构_zend_reference {
zend_refcounted GC;
Zval Val;
};


在本质上,zend_reference只有zval增加引用计数。所有引用变量将存储一个变量指针和被标记为is_reference.val和其他变量的行为一样,特别是它可以共享复杂的变量的指针,它的商店,例如,数组可以共享变量和变量之间的参考价值。

让我们在实例看,这一次是php7语义。为了简单起见,变量不再单独写,只显示结构,他们指出:


< PHP
美元= {}; / /美元-> zend_array_1(引用计数为1,价值= { })
$ = $ / /;$,$,zend_reference_1(引用计数= 2)- zend_array_1(引用计数为1,价值= { })
$ { } = 1; / / $,$,zend_reference_1(引用计数= 2)- zend_array_1(引用计数为1,价值= { 1 })




在上面的例子中,一个zend_reference创造传球时的参考。请注意,它的引用计数为2(因为两个变量是使用PHP参考)。但对价值本身的引用计数为1(因为zend_reference只有一个指向它的指针),这是在一个参考和非参考混合物的情况看:


< PHP
美元= {}; / /美元-> zend_array_1(引用计数为1,价值= { })
$ = $ / /;$,$,zend_array_1(引用计数为2,价值= { })->
C = B /美元美元美元美元美元,B,C,zend_array_1(引用计数为3,价值= { })
美元D = $ C; / / $,$,zend_array_1(引用计数为3,价值= { })
/ / $ C,D zend_reference_1美元(引用计数= 2)--- ^
请注意,所有的变量或共享相同的zend_array,如果有参考PHP或不
美元D { } = 1; / / $,$,zend_array_1(引用计数为2,价值= { })
/ / $ C,D(引用计数= 2)-> zend_reference_1 -> zend_array_2(引用计数为1,价值= { 1 })
/ /分配给zend_array只有当分配


最大的区别在PHP5是所有的变量可以共享相同的阵列,即使一些PHP数组引用是不分离。只有当一部分修改。这也意味着它是安全的使用计数()甚至通过一个大的参考阵列,而不是复制一次。然而,报价仍然比普通值慢,因为需要分配内存(间接)的zend_reference结构和发动机本身不好。

后记

在php7最重要的变化是,不再单独distributies zval从堆内存,本身并不存储引用计数。复杂类型(如字符串、数组和对象),需要使用zval指针将存储的引用计数本身。这使得更少的内存分配操作,减少间接指针的用法,和更少的内存分配。

在下一篇文章中,在php7介绍变量的实现(二),感兴趣的朋友继续关注它。