約束どおり、私もこのシリーズの記事を書き始めます。 zval の変更点について話す前に、zval について見てみましょう。 PHP5 ではどのようになりますか
PHP5 では、zval の定義は次のとおりです:
struct _zval_struct { union { long lval; double dval; struct { char *val; int len; } str; HashTable *ht; zend_object_value obj; zend_ast *ast; } value; zend_uint refcount__gc; zend_uchar type; zend_uchar is_ref__gc;};
zval はすべてのデータを表すことができるため、PHP5 カーネルを知っている学生はこの構造に精通しているはずです。したがって、この zval が格納する値のタイプを示す type フィールドが含まれています。一般的なオプションは、IS_NULL、IS_LONG、IS_STRING、IS_ARRAY、IS_OBJECT などです。
type フィールドの値に応じて、次のことを行う必要があります。 use Different value の値を共用体として解釈します。たとえば、type が IS_STRING の場合は、value.str を使用して zval.value フィールドを解釈する必要があり、type が IS_LONG の場合は、value を使用する必要があります。
さらに、PHP は基本的なガベージ コレクションに参照カウントを使用することがわかっているため、zval には refcount__gc フィールドがあり、この zval への参照の数を示しますが、ここで注意すべき点が 1 つあります。 5.3、このフィールドの名前は refcount とも呼ばれます。5.3 以降、循環参照カウントを処理するために新しいガベージ コレクション アルゴリズムが導入されたとき、作成者はエラーの表示を高速化するために多数のマクロを追加しました。 refcount__gc という名前に変更され、refcount を操作するように強制されました。
同様に、この値は PHP の型が参照であるかどうかを示します。これは、PHP5 時代の zval です。2013 年に PHP5 の opcache JIT に取り組んでいたとき、JIT のパフォーマンスが低かったため、この構造を書き直すことから PHPNG プロジェクトが始まりました。問題
PHP5 の zval 定義は Zend Engine 2 で誕生しました。時間が経つにつれて、当時の設計の限界がますます明らかになりました:まず第一に、この構造のサイズは (64 ビット システム上では) ) 24 バイト、この zval.value の組み合わせを詳しく見てみましょう。その中で、zend_object_value が最大の長いボードです。これにより、値全体が 16 バイト必要になります。これは、移動して置き換えるなどの最適化が簡単です。結局のところ、IS_OBJECT は最も一般的に使用されるタイプではないためです。
第二に、この構造体の各フィールドには明確な意味があり、その結果、多くの最適化を行う場合にはカスタム フィールドが予約されません。 PHP5 の時代では、zval に関連する情報を保存する必要がある場合、zval を拡張するために他の構造マッピング、または外部のパッケージ化とパッチを使用する必要がありました。たとえば、5.3 では循環参照を解決するために新しい GC が導入されました。次の比較を使用してはなりません ハックメソッド:
/* The following macroses override macroses from zend_alloc.h */ #undef ALLOC_ZVAL#define ALLOC_ZVAL(z) \ do { \ (z) = (zval*)emalloc(sizeof(zval_gc_info)); \ GC_ZVAL_INIT(z); \ } while (0)
これは、zval_gc_info を使用して 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 を展開します。つまり、実際に PHP5 時代に zval を適用するときは、 、実際には 32 バイトを割り当てますが、実際には GC は IS_ARRAY と IS_OBJECT タイプのみを考慮する必要があるため、大量のメモリを浪費します
また、以前に作成した Taint 拡張機能と同様に、いくつかのタグを保存する必要があります。いくつかの文字列があり、zval には場所がありません。これは使用できるので、特別な手段を使用する必要がありました:
Z_STRVAL_PP(ppzval) = erealloc(Z_STRVAL_PP(ppzval), Z_STRLEN_PP(ppzval) + 1 + PHP_TAINT_MAGIC_LENGTH); PHP_TAINT_MARK(*ppzval, PHP_TAINT_MAGIC_POSSIBLE);
文字列の長さを int で拡張してから、マジックナンバーでマークして書き込むだけです。このアプローチは技術的にはより安全で安定しているという保証はありません
第三に、PHP の zval のほとんどは値によって渡され、値は書き込み時にコピーされます。ただし、オブジェクトとリソースの 2 つの例外があります。それらは常に参照によって渡されるため、zval の参照カウントに加えて、オブジェクトとリソースもメモリを確実にリサイクルできるようにするためのグローバル参照カウントが必要になります。たとえば、これには 2 つの参照カウントのセットがあり、1 つは zval 内にあり、もう 1 つは obj 自体のカウントです。
typedef struct _zend_object_store_bucket { zend_bool destructor_called; zend_bool valid; union _store_bucket { struct _store_object { void *object; zend_objects_store_dtor_t dtor; zend_objects_free_object_storage_t free_storage; zend_objects_store_clone_t clone; const zend_object_handlers *handlers; zend_uint refcount; gc_root_buffer *buffered; } obj; struct { int next; } free_list; } bucket;} zend_object_store_bucket;
上記の 2 つの参照セットに加えて、オブジェクトを取得したい場合は、次の方法で行う必要があります:
EG(objects_store).object_buckets[Z_OBJ_HANDLE_P(z )].bucket.obj
長くて多くのメモリ読み取りの後、実際のオブジェクトオブジェクト自体を効率的に取得できます。
これはすべて、Zend エンジンが最初に設計されたときに考慮されていなかったためであり、一度事故が発生すると、全体の構造が複雑になり、保守性が低下します。例
4 番目に、PHP では多くの計算が文字列指向であることがわかっていますが、参照カウントは zval で機能するため、文字列型の zval をコピーしたい場合は何も存在しません。 PHP5.4でINTERNED STRINGを導入しましたが、zval文字列を配列に追加する場合は文字列をコピーするしかありません。この問題
还比如, PHP中大量的结构体都是基于Hashtable实现的, 增删改查Hashtable的操作占据了大量的CPU时间, 而字符串要查找首先要求它的Hash值, 理论上我们完全可以把一个字符串的Hash值计算好以后, 就存下来, 避免再次计算等等
第五, 这个是关于引用的, PHP5的时代, 我们采用写时分离, 但是结合到引用这里就有了一个经典的性能问题:
<?php function dummy($array) {} $array = range(1, 100000); $b = &$array; dummy($b); ?>
当我们调用array_count的时候, 本来只是简单的一个传值就行的地方, 但是因为$b 是一个引用, 就必须发生分离, 导致数组复制, 从而极大的拖慢性能, 这里有一个简单的测试:
<?php $array = range(1, 100000); function dummy($array) {} $i = 0; $start = microtime(true); while($i++ < 100) { dummy($array); } printf("Used %sS\n", microtime(true) - $start); $b = &$array; //注意这里, 假设我不小心把这个Array引用给了一个变量 $i = 0; $start = microtime(true); while($i++ < 100) { dummy($array); } printf("Used %sS\n", microtime(true) - $start); ?>
我们在5.6下运行这个例子, 得到如下结果:
$ php-5.6/sapi/cli/php /tmp/1.phpUsed 0.00045204162597656SUsed 4.2051479816437S
相差1万倍之多. 这就造成, 如果在一大段代码中, 我不小心把一个变量变成了引用(比如foreach as &$v), 那么就有可能触发到这个问题, 造成严重的性能问题, 然而却又很难排查.
第六, 也是最重要的一个, 为什么说它重要呢? 因为这点促成了很大的性能提升, 我们习惯了在PHP5的时代调用MAKE_STD_ZVAL在堆内存上分配一个zval, 然后对他进行操作, 最后呢通过RETURN_ZVAL把这个zval的值"copy"给return_value, 然后又销毁了这个zval, 比如pathinfo这个函数:
PHP_FUNCTION(pathinfo){..... MAKE_STD_ZVAL(tmp); array_init(tmp);.... if (opt == PHP_PATHINFO_ALL) { RETURN_ZVAL(tmp, 0, 1); } else {.....}
这个tmp变量, 完全是一个临时变量的作用, 我们又何必在堆内存分配它呢? MAKE_STD_ZVAL/ALLOC_ZVAL在PHP5的时候, 到处都有, 是一个非常常见的用法, 如果我们能把这个变量用栈分配, 那无论是内存分配, 还是缓存友好, 都是非常有利的
还有很多, 我就不一一详细列举了, 但是我相信你们也有了和我们当时一样的想法, zval必须得改改了, 对吧?
到了PHP7中, zval变成了如下的结构, 要说明的是, 这个是现在的结构, 已经和PHPNG时候有了一些不同了, 因为我们新增加了一些解释 (联合体的字段), 但是总体大小, 结构, 是和PHPNG的时候一致的:
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; zend_ast_ref *ast; 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, /* 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;};
虽然看起来变得好大, 但其实你仔细看, 全部都是联合体, 这个新的zval在64位环境下,现在只需要16个字节(2个指针size), 它主要分为俩个部分, value和扩充字段, 而扩充字段又分为u1和u2俩个部分, 其中u1是type info, u2是各种辅助字段.
其中value部分, 是一个size_t大小(一个指针大小), 可以保存一个指针, 或者一个long, 或者一个double.
而type info部分则保存了这个zval的类型. 扩充辅助字段则会在多个其他地方使用, 比如next, 就用在取代Hashtable中原来的拉链指针, 这部分会在以后介绍HashTable的时候再来详解.
类型PHP7中的zval的类型做了比较大的调整, 总体来说有如下17种类型:
/* 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 /* fake types */ #define _IS_BOOL 13 #define IS_CALLABLE 14 /* internal types */ #define IS_INDIRECT 15 #define IS_PTR 17
其中PHP5的时候的IS_BOOL类型, 现在拆分成了IS_FALSE和IS_TRUE俩种类型. 而原来的引用是一个标志位, 现在的引用是一种新的类型.
对于IS_INDIRECT和IS_PTR来说, 这俩个类型是用在内部的保留类型, 用户不会感知到, 这部分会在后续介绍HashTable的时候也一并介绍.
从PHP7开始, 对于在zval的value字段中能保存下的值, 就不再对他们进行引用计数了, 而是在拷贝的时候直接赋值, 这样就省掉了大量的引用计数相关的操作, 这部分类型有:
IS_LONGIS_DOUBLE
当然对于那种根本没有值, 只有类型的类型, 也不需要引用计数了:
IS_NULLIS_FALSEIS_TRUE
而对于复杂类型, 一个size_t保存不下的, 那么我们就用value来保存一个指针, 这个指针指向这个具体的值, 引用计数也随之作用于这个值上, 而不在是作用于zval上了. 以IS_ARRAY为例:
struct _zend_array { zend_refcounted_h gc; union { struct { ZEND_ENDIAN_LOHI_4( zend_uchar flags, zend_uchar nApplyCount, zend_uchar nIteratorsCount, zend_uchar reserve) } v; uint32_t flags; } u; uint32_t nTableMask; Bucket *arData; uint32_t nNumUsed; uint32_t nNumOfElements; uint32_t nTableSize; uint32_t nInternalPointer; zend_long nNextFreeElement; dtor_func_t pDestructor;};
zval.value.arr将指向上面的这样的一个结构体, 由它实际保存一个数组, 引用计数部分保存在zend_refcounted_h结构中:
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;
所有的复杂类型的定义, 开始的时候都是zend_refcounted_h结构, 这个结构里除了引用计数以外, 还有GC相关的结构. 从而在做GC回收的时候, GC不需要关心具体类型是什么, 所有的它都可以当做zend_refcounted*结构来处理.
另外有一个需要说明的就是大家可能会好奇的ZEND_ENDIAN_LOHI_4宏, 这个宏的作用是简化赋值, 它会保证在大端或者小端的机器上, 它定义的字段都按照一样顺序排列存储, 从而我们在赋值的时候, 不需要对它的字段分别赋值, 而是可以统一赋值, 比如对于上面的array结构为例, 就可以通过:
arr1.u.flags = arr2.u.flags;
一次完成相当于如下的赋值序列:
arr1.u.v.flags = arr2.u.v.flags;arr1.u.v.nApplyCount = arr2.u.v.nApplyCount;arr1.u.v.nIteratorsCount = arr2.u.v.nIteratorsCount;arr1.u.v.reserve = arr2.u.v.reserve;
もう 1 つの考えられる疑問は、なぜ zval 型の前に型 type を置かないのかということです。なぜなら、zval を使用するとき、最初にその型を取得する必要があることがわかっているからです。理由の 1 つは、はい、1 つです。両者に大きな違いはありません。もう 1 つは、将来 JIT が実装された場合、zval の型が型推論によって取得できる場合、その型の値を読み取る必要がなくなることです。続き)