php7变量实现_zval

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的,如a=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;
php7_reference.png

上面的_zend_reference和_zend_string实例中gc计数均为2。

参考

深入理解PHP原理之变量(Variables inside PHP)

深入理解PHP原理之变量作用域(Scope in PHP)

深入理解PHP原理之变量分离/引用(Variables Separation)

深入理解PHP7内核之zval

PHP7源码学习 2019-03-13 PHP字符串笔记

PHP内核pemalloc的persistent硬编码为1,我们该如何去进行释放

PHP内核分析之深入理解字符串(七)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350