PHP-NG版实现细节 ( PHP - NGImplementationDetails ) 译者:花生 (360电商技术组) 本文为大家提供关于 PHPNG 内部实现的一些技术细节。关于 PHPNG 的基本信息在 phpng ,为扩展维护者提供的一些信息在 phpng-upgrading ValueRepresentation 在当前的 Zend
PHP-NG版实现细节
(PHP-NG Implementation Details)
译者:花生 (360电商技术组)
本文为大家提供关于PHPNG内部实现的一些技术细节。关于PHPNG的基本信息在phpng,为扩展维护者提供的一些信息在phpng-upgrading
Value Representation
在当前的Zend Engine的实现当中,所有的值都是在堆上分配空间,并且通过引用计数和垃圾收集来管理。Zend Engine主要使用指向zval结构的指针来操作值(在很多地方甚至通过zval的二级指针)
新Zend Engine的实现中,值是通过zval结构本身来操作(非指针)。新的zval结构直接被存在VM的栈上、HashTable的桶里、以及属性槽里(property slots)。这样大大减少了在堆上分配和释放内存的操作。它还避免了对简单值的引用计数和垃圾收集(eg. null,bool,long,double,interned string,immutable arrays)。
新Zend Engine使用了大量的VM栈空间(而不是堆),因为它现在保存的是zval结构而不是之前的指针。不管怎么说,降低了整体的内存利用率。在某些情况下,新的Zend Engine使用完全的zval复制方式来替代之前的copy-on-write机制,但是这样做并没有带来性能的损耗(它需要两份内存读和两份内存写,而不是单个内存读写加上引用计数器递增的方式,在那之前,会导致相同的两份读写)。
CELL Format(zval)
由两个64位字段的组合来表示一个值的单元。第一个字段包含实际的值(它由可能的值的类型定义为一个联合体union);第二个字段包含类型标记和一些其他的标记。为了提高效率,这些类型和标记可能会被当做一个单一的32位的字段来进行使用。当值单元被嵌入到其他结构时,那些“未使用的”空间实际上可以被重新利用起来以达到不同的目的。(eg. 当值插入到HashTable产生hash冲撞生成链表的时候)
经过重构的Zend Engine定义了下面这些数据类型。这些类型大多都是PHP-5引擎中众所周知的:
· IS_UNDEF - 对于未定义的变量我们使用一个特殊的类型
· IS_NULL
· IS_FALSE - 我们将IS_BOOL拆分成了独立的IS_FALSE和IS_TRUE
· IS_TRUE
· IS_LONG
· IS_DOUBLE
· IS_STRING - 普通或者临时字符串
· IS_ARRAY - 普通或者不变数组
· IS_OBJECT
· IS_RESOURCE
· IS_REFERENCE - 一个单独的引用类型(稍后解释它)
· IS_CONSTANT - 已命名的常数
· IS_CONSTANT_AST - 常数表达式
· IS_CALLABLE - 仅用于类型提示
· _IS_BOOL - 仅用于类型提示
· IS_INDIRECT - 用于处理其他值的指针的特殊用途类型
· IS_STR_OFFSET - 用于处理字符串的偏移量的特殊用途类型(仅仅在VM中使用)
· IS_PTR - 某些东西的指针(eg. zend_function,zend_class_entry,etc)
除了类型本身,引擎定义了几个类型标志以类似的行为统一处理不同的数据类型。
· IS_TYPE_CONSTANT - 常数类型(IS_CONSTANT,IS_CONSTANT_AST)
· IS_TYPE_REFCOUNTED - 用于引用计数的(IS_STRING不包含临时字符串,IS_ARRAY不包含不可变数组,IS_OBJECT,IS_RESOURCE,IS_REFERENCE)。使用引用计数的所有类型的值是一个指向具有相同的部件的结构的指针(zend_refcounted)。使用宏Z_COUNTED可以获得这种结构,或者通过使用宏Z_GC_TYPE()、Z_GC_FLAGS()、G_GC_INFO()、Z_GC_TYPE_INFO()来获得此结构中的一些数据。通过使用宏Z_REFCOUNT()、Z_SET_REFCOUNT()、Z_ADDREF()、Z_DELREF()可以访问引用计数器。
· IS_TYPE_COLLECTABLE - 未引用周期的根,用于垃圾回收(IS_ARRAY,IS_OBJECT)。(the type may be a root of unreferenced cycle and it's a subject for Garbage Collection)
· IS_TYPE_COPYABLE - 在分配或者写时复制时必须多次使用zval_copy_ctor()(IS_STRING排除临时字符串, IS_ARRAY)
· IS_TYPE_IMMUTABLE - 不能直接修改,但在写的时候会被复制。被用于避免不可变数组不必须要的复制。
几个常量标志作为修饰符用于IS_CONSTANT。他们存在的意义在于和以前保持高度一致
· IS_CONSTANT_UNQUALIFIED
· IS_LEXICAL_VAL
· IS_LEXICAL_REF
· IS_CONSTANT_IN_NAMESPACE
宏Z_TYPE()或者Z_TYPE_P()访问zval的类型,宏Z_TYPE_FLAGS()或Z_TYPE_FLAGS_P()访问类型标志,宏Z_TYPE_INFO()或Z_TYPE_INFO_P()访问类型和标志的组合。PHPNG不能使用zval的二级指针,并且它不再提供以_PP()为后缀的宏(类似Z_TYPE_PP)。
IS_UNDEF
未定义的IS_CV变量或者空的哈希表必须使用特殊类型IS_UNDEF(以前他们被初始化为NULL指针)。引擎仅仅在几个地方需要需要支持IS_UNDEF类型。用户脚本不能获得或者使用未定义的值,他们看到的还是NULL。
使用宏ZVAL_UNDEF()初始化未定义的值。
IS_NULL
使用宏ZVAL_NULL()初始化null值
IS_FALSE and IS_TRUE
将老的IS_BOOL类型分拆成了独立的IS_FALSE和IS_TRUE类型。现在这两个值就可以用相同的类型标记来检查,避免了额外的内存读取。布尔值的初始化也和以前一样使用宏ZVAL_BOOL()、ZVAL_FALSE或者ZVAL_TRUE()
IS_LONG
实际使用中,使用宏Z_LVAL()或Z_LVAL_P()来获取长整形的值,使用宏ZVAL_LONG()初始化长整形变量。
IS_DOUBLE
实际使用中,使用宏Z_DVAL()或Z_DVAL_P()来获取浮点型的值,使用宏ZVAL_DOUBLE()初始化浮点型变量。
IS_STRING
zend_string:
这种类型实际的值是保存在zend_string结构体中,zval只是保存了指向它的指针。这个结构中第一个64位的词实际上是zend_refcounted结构。它由引用计数器、由多个zval类型组合而成的复杂类型(可能的一些变形)、额外的标识以及一些在GC过程中使用过的数据组成。
字符串本身就表示了hash值(它不必被计算,它用0初始化并且根据要求计算,但是仅仅只有一次),
zend_string结构包含由它本身的值的hash_value(这个hash值不必人为计算,它使用0来初始化并根据要求来计算hash值,这种计算hash值的过程只会发生一次)、字符串长度以及实际的字符串值。
字符串可以使动态的或者静态的。对于静态字符串,我们不需要执行引用计数或者复制,从另一个方面讲,他们不能被修改。
一下附加的标志可能会被用于字符串:
· IS_STR_PERSISTENT - 使用malloc分配内存的时候(也可以是emalloc)
· IS_STR_INTERNED - 静态字符串
· IS_STR_PERMANENT - interned string that relives request boundary
· IS_STR_CONSTANT - 不变索引(constant index)
· IS_STR_CONSTANT_UNQUALIFIED - 同IS_CONSTANT_UNQUALIFIED
· IS_STR_AST - constant expression index
访问字符串变量的值和属性可以使用宏Z_STRVAL()、Z_STRLEN()、Z_STRHASH()、Z_STR(),字符串变量的初始化可以使用宏ZVAL_STRINGL()、ZVAL_STRING()、ZVAL_STR()、ZVAL_INT_STR()或者ZVAL_NEW_STR()。
在Zend Engine中的很多地方使用zend_string*来替代char*
IS_ARRAY
Zend_array:
HashTable:
Bucket:
数组的结构和以前的有很多相同的地方。我们将引用计数器从以前的zval结构中挪到了zend_array结构体中(类似zend_string),以及这里新定义了Embedded HashTable,但是对HashTable的访问的成本却没有变。对于数组工作机制来说,仍然使用写时复制(copy-on-write)(递增引用计数),而不是直接复制。
对比新老HashTable的表示,明显的改变了很多。首先,现在,它是一个自适应的数据结构,使用简单的预先分配好的Buckets数组,并且只有在必要时候才构造hash索引(在某些情况下,它可能使用它自己的索引来访问数组,比如在C数组中)。
下面定义了一些HashTable的标志:
· HASH_FLAG_PERSISTENT - HashTable must relive the request boundary and its data must be allocated using malloc()
· HASH_FLAG_APPLY_PROTECTION - 检测间接递归
· HASH_FLAG_PACKED - this is not really a hash but a plain array with numeric indexes (arHash is NULL)
zend_array和embedded HashTable使用宏Z_ARR和Z_ARRVAL访问,使用宏ZVAL_ARR()、ZVAL_NEW_ARRAY()以及ZVAL_PERSISTENT_ARRAY来创建。
IS_OBJECT
Zend_object:
对象的结构也被更改了很多。我们去掉了需要跳转两次(访问)对象存储的句柄以及双重引用。我们保持了对象句柄的兼容性。预定义的属性存储在嵌入结构中,与zend_object一起分配内存(Predefined properties are stored in embedded cells allocated together with zend_object structure)。动态属性表默认为NULL。它在需要的时候才去构造。在何种情况下,它会包含指向基本embedded cells的IS_INDIRECT引用。
可以获取对象的内容的宏有Z_OBJ()、Z_OBJ_HT()、Z_OBJ_HANDLER()、Z_OBJ_HANDLE()、Z_OBJCE()、Z_OBJPROP()、Z_OBJDEBUG()。对象可以使用函数object_init()或者object_init_ex()来构造。
在运行过程中,zend_object结构只存在一份,不会被复制,仅仅是递增引用计数器。
IS_RESOURCE
zend_resource:
对于资源类型的表示,我们现在使用直接的指针避免两次引用计数(保持兼容性句柄)。
资源类型数据使用宏Z_RES()、Z_RES_HANDLE()、Z_RES_TYPE以及Z_RES_VAL()访问数据。使用宏ZVAL_RES()、ZVAL_NEW_RES()、ZVAL_PERSISTENT_RES初始化资源类型变量。
IS_REFERENCE
zend_reference:
最显著的变化就是对于引用的处理。在以前,我们是在zval结构里面使用“is_ref”标志;现在,实际的引用的值是被保存在单独的拥有自己的引用计数器的zend_reference结构中。所有的在zval中的引用只是保存了一个指向zend_referenece结构的指针。
Note:引用的值可能是另一种标量或引用计数的值(IS_STRING,IS_ARRAY,IS_OBJECT,IS_RESOURCE),但不可能是另一个IS_REFERENCE或者IS_UNDEF。
Note:如今当PHP的引用计数器降到1的时候,引用可能会简单的变成常规值。在新的实现当中,它不是微不足道的操作。
检查一个值是否为引用使用宏Z_ISREF(),使用宏Z_REF()和Z_REFVAL读取引用的值。使用宏ZVAL_REF()、ZVAL_NEW_REF()、ZVAL_NEW_PERSISTENT_REF()创建一个引用。
IS_REFERENCE
IS_CONSTANT实际上是指向zend_string的一个指针。他们的处理有点不同于常规字符串和数组。
IS_INDIRECT
新的实现假定我们将zval结构(非指针)存储在数组和函数的栈帧中。对于数组来说,这个肯定不是问题,因为标量值仅仅会被复制,而复合值可以指向共享的引用计数结构。而对于本地变量(IS_CV)来说肯定是一个问题,因为他们可能会通过栈帧(通过索引)和符号表(通过名称)来引用。两者都必须指向相同的机构。IS_INDIRECT类型的值都只是使用弱指针指向真实的值。当我们慢吞吞地创建本地符号表时,我们将IS_INDIRECT值存在符号表中,并且使用指向相应的CV槽的指针初始化他们。这就意味着我们使用索引访问CV变得更高效,因为我们不需要像以前一样执行两次甚至三次取消引用的操作。
全局符号表的处理有点不同。当我们在用户代码中使用全局变量时,我们需要将他们从 EG(symbol_table) 中拷贝到CV槽中,然后用IS_INDIRECT指针初始化符号表中的值。在退出的时候我们需要将他们还原回来。
同样的思路也被用在了对象属性的访问上。以防万一动态属性表在第一次初始化时候使用预定义对象属性槽的IS_INDIRECT引用。
此外,IS_INDIRECT指针也被用于在VM执行期间在opcode的处理函数之间传递变量的地址。
IS_STR_OFFSET (used internally in VM)
这是另一种只在运行时用于操作码之间传递字符串元素地址的类型。
IS_PTR (used internally by the Engine)
对于一些内部的元素,这种类型可以用于重复使用新的HashTable的实现.而不涉及PHP的值(eg.每一个zend_class_entry必须保持一个methods的HashTable)。
VM Changes
在新的zval的实现中,IS_TMP_VAR、IS_VAR以及IS_CV这些操作符的处理方式都非常相似。这三个操作符都仅仅只涉及到当前函数栈帧的特定的槽。这种槽被分配在分段的VM栈上,与帧头在一起(zend_execute_data)。第一个槽对应CV变量,其他槽对应IS_TMP_VAR和IS_VAR。除了局部和临时变量,我们也为语法嵌套的函数调用和实际参数分配内存空间,that this function may push。
Function Stack Frame (zend_execute_data)
-------------------------------------------------------------------------------------
黑夜路人,一个关注开源技术、乐于学习、喜欢分享的程序员
博客:http://blog.csdn.net/heiyeshuwu
微博:http://weibo.com/heiyeluren
微信:heiyeluren2012
想获取更多IT开源技术相关信息,欢迎关注微信!
微信二维码扫描快速关注本号码:
作者:heiyeshuwu 发表于2014-10-24 15:58:19 原文链接
阅读:478 评论:0 查看评论