深入剖析PHP7内核源码(二)- PHP变量容器

简介

PHP的变量使用起来非常方便,其基本结构是底层实现的zval,PHP7采用了全新的zval,由此带来了非常大的性能提升,本文重点分析PHP7的zval的改变。

PHP5时代的ZVAL

typedef struct _zval_struct {
    zvalue_value value; // (长度16字节,具体看下面的分析)
    zend_uint refcount__gc; //  unsigned int (长度4字节)
    zend_uchar type;  //  unsigned char (长度1字节)
    zend_uchar is_ref__gc;  //  unsigned char (长度1字节)
} zval

typedef union _zvalue_value {
    long lval;                 // 用于 bool 类型、整型和资源类型(长度8字节)
    double dval;               // 用于浮点类型(长度8字节)
    struct {                   // 用于字符串
        char *val;    // 字符串指针(长度8字节)
        int len;   //字符串长度(长度4字节)
    } str;
    HashTable *ht;             // 用于数组(长度8字节)
    zend_object_value obj;     // 用于对象(12字节)
    zend_ast *ast;             // 用于常量表达式(长度8字节)
} zvalue_value;
  • zvalue_value 是联合体,长度取最大的一个,为12字节,内存对齐后是16字节(需要对齐为8的倍数)。
  • zval 是结构体,长度是各个变量的总和,为22字节,内存对齐后是24字节。
  • php5.3后对zval进行了扩充,解决循环引用的问题,因此实际上申请一个变量分配了 24 + 8 = 32字节的内存。
typedef struct _zval_gc_info {
    zval z;
    union {
        gc_root_buffer       *buffered;
        struct _zval_gc_info *next;
    } u;   // (长度8字节)
} zval_gc_info;

所以在PHP里面,给一个变量赋值,实际上会转换成这样来运行

<?php
$var = 123
=>
zval.value = 123
zval.type = IS_LONG
zval.refcount__gc= 0
zval.is_ref__gc = 0
... 

PHP7 时代的ZVAL

struct _zval_struct {
     union {
          zend_long         lval;   // 整型(长度8字节)
          double            dval;       // 浮点型(长度8字节)
          zend_refcounted  *counted; // 引用计数(长度8字节)
          zend_string      *str; // 字符串类型(长度8字节)
          zend_array       *arr; // 数组(长度8字节)
          zend_object      *obj; // 对象(长度8字节)
          zend_resource    *res; // 资源型(长度8字节)
          zend_reference   *ref; // 引用型(长度8字节)
          zend_ast_ref     *ast; //抽象语法树(长度8字节)
          zval             *zv; // zval类型(长度8字节)
          void             *ptr; // 指针类型(长度8字节)
          zend_class_entry *ce; // class类型(长度8字节)
          zend_function    *func; // function类型(长度8字节)
          struct {
               uint32_t w1;   // (长度4字节)
               uint32_t w2;  // (长度4字节)
          } ww;  // 长度8字节
     } value;  // 因为是联合体,所以实际上整个value只用了8字节

    union {
        struct {
            ZEND_ENDIAN_LOHI_4(
                zend_uchar    type,       // zval的类型(长度1字节)
                zend_uchar    type_flags, //对应变量类型特有的标记(长度1字节)
                zend_uchar    const_flags, // 常量类型标记(长度1字节)
                zend_uchar    reserved)     // 保留字段(长度1字节)
        } v; // 总共长度是4字节
        uint32_t type_info; //  其实就是v的值位运算结果(长度4字节)
    } u1; // u1也是联合体,总共长度4字节

    union {
        uint32_t     var_flags;
        uint32_t     next;                 // 用来解决哈希冲突的(长度4字节)
        uint32_t     cache_slot;      // 运行时缓存(长度4字节)
        uint32_t     lineno;              // zend_ast_zval行号(长度4字节)
        uint32_t     num_args;         // Ex(This) 参数个数(长度4字节)
        uint32_t     fe_pos;              // foreach 的位置(长度4字节)
        uint32_t     fe_iter_idx;         // foreach 迭代器游标(长度4字节)
    } u2;  // u2也是联合体,总共长度4字节
};
  • value (8) + u1(4) +u2(4) = 16,整个变量才用了16字节,相比PHP5来说,节省了一半内存。
  • value 保存具体是值,不同的类型的值,用的是联合体的同一块空间。
  • u1 变量的类型就通过u1.v.type区分,另外一个值type_flags为类型掩码,在变量的内存管理、gc机制中会用到
  • u2 辅助值,假如zval只有:value、u1两个值,整个zval的大小也会对齐到16byte,所以加了u2作为辅助,比如next在哈希表解决哈希冲突时会用到,还有fe_pos在foreach会用到

zvalue的类型

zvalue.u1.type

/* 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_AST             11

/* internal types (伪类型)*/
#define IS_INDIRECT                 13
#define IS_PTR                      14
#define _IS_ERROR                   15

/* fake types used only for type hinting (Z_TYPE(zv) can not use them)  内部类型*/
#define _IS_BOOL                    16
#define IS_CALLABLE                 17
#define IS_ITERABLE                 18
#define IS_VOID                     19
#define _IS_NUMBER                  20
  • PHP是根据u1.v.type的类型取不同的值,比如u1.v.type == IS_LONG,则取值 value.lval
  • IS_UNDEF 未定义,表示数据可以被删除,可用于对数组unset的时候标记Bucket的位置为IS_UNDEF,等标记元素达到阈值的时候,进行rehash操作删除数据
  • IS_TRUE IS_FALSE 将PHP5时代的IS_BOOL分开为两个,只需要一次操作即可取值。
  • IS_REFERENCE 处理&变量
  • IS_INDIRECT 解决全局符号表访问CV变量表
  • IS_PTR 指针类型,解释 value.ptr,通常用在函数类型上,比如声明一个函数
  • _IS_ERROR 检查zval的类型是否合法

字符串的实现

struct _zend_string {
    zend_refcounted_h gc;  // 引用计数,变量引用信息
    zend_ulong        h;          // 哈希值,数组中计算索引时会用到
    size_t            len; // 字符串长度
    char              val[1]; //  字符串内容
};
  • zend_ulong h 缓存了字符串的hash值,避免了数组中的重复计算字符串hash,提升了5%的性能
  • val值储存字符串类型,用的是柔性数组类型

zval.value->gc.u.flags 这个标记代表了下面几种不同类型的字符串

IS_STR_PERSISTENT(通过malloc分配的)
IS_STR_INTERNED(php代码里写的一些字面量,比如函数名、变量值)
IS_STR_PERMANENT(永久值,生命周期大于request)
IS_STR_CONSTANT(常量)
IS_STR_CONSTANT_UNQUALIFIED

整数的实现

整数是标量,在容器中zval直接存储

$a = 666;
// $a = zval_1(u1.v.type=IS_LONG,value.lval=666)

$b = $a;
// $a = zval_1(u1.v.type=IS_LONG,value.lval=666)
// $b = zval_2(u1.v.type=IS_LONG,value.lval=666)

unset($a);
// $a = zval_1(u1.v.type=IS_UNDEF,value.lval=666)
  • PHP7相对于PHP5 的一个改变就是,对标量的值直接拷贝,而没有做写时拷贝,因为zval只有16字节,写时拷贝实际上节省不了内存还会增加操作的复杂度。
  • unset的时候把 u1.v.type 标记为IS_UNDEF,内存不会释放。

数组的全貌

数组的基本结构是基于key value的 HashTable,同时是一个双向链表。熟悉数据结构的都知道,对一个字符串Hash的时候有可能产生哈希冲突,PHP是怎么解决的?当发生冲突的时候,PHP在该映射后面会加上一条链表,哈希冲突后就会从链表中找值。使用了双向链表的好处是,我们对数组最常用的操作就是遍历数组,通过双向链表,我们可以很方便进行遍历。你可能会问,那如果仅仅是这样,单向链表不也解决了吗?还节省点空间。实际上,之所以用双向链表的一个原因,是因为链表在删除元素的时候,就必须找到上一个元素,把它的指针指向到下下个元素,双向链表已经储存了上一个元素的指针,而单向链表就必须遍历整个HashTable,时间复杂度将会是很差的O(n)。

  • HashTable删除元素的时间复杂度是O(1),双向链表删除的时间复杂度也是O(1),所以整个删除操作可以做到时间最优的O(1)。

这个是PHP数组的大概样子,后面会专门写一篇来概述是数组HashTable的实现。

资源类型

PHP中很多依赖外部的操作都是资源类型,比如文件资源 Socket连接资源,资源类型的定义如下

struct _zend_resource{
    zend_refcounted_h gc;
    int    handle;
    int    type;
    void    *ptr; //指针,根据使用场景转换为任何类型
}

对象类型

struct _zend_object {
    zend_refcounted_h gc;
    uint32_t          handle;
    zend_class_entry *ce; //对象对应的class类
    const zend_object_handlers *handlers;
    HashTable        *properties; //对象属性哈希表
    zval              properties_table[1];
};

properties 是一个HashTable ,key 对象的属性 ,value是对象在properties_table 数组中的偏移量,值真正的位置是在properties_table 数组中。

引用类型

PHP的引用类型是比较特殊的一种类型,可以通过 & 操作符可以产生一个引用变量,假如把 $b = &a; $b 的值改变的时候,$a 的值也跟着改变。

struct _zend_reference {
    zend_refcounted_h gc;
    zval              val;
};
  • zend_refcounted_h 结构体用来储存引用计数的信息
  • val 存储的是实际的值
$a = "time:" . time();      //$a    -> zend_string_1(refcount=1)
$b = &$a;                 //$a,$b -> zend_reference_1(refcount=2) -> zend_string_1(refcount=1)
$c = $b;                  //$a,$b -> zend_reference_1(refcount=2) -> zend_string_1(refcount=2)
                               //$c    ->   zend_string_1(refcount=2)
  1. $a 赋值字符串,zend_string_1 的引用计数记为1。
  2. 把$a的引用赋值给$b,zend_string_1 结构的引用计数不变,产生了一个中间结构体zend_reference_1,该结构体的引用计数为2。
  3. $b 赋值给$c ,zend_reference_1引用计数不变,zend_string_1引用计数记为2。

中间结构体zend_reference_1存在的好处是,zend_string只需要存一份,减少空间的浪费以及申请空间带来的额外开销

附录

什么是内存对齐

比如数据总线有32位,它访存只能4个字节4个字节地进行。 0-3,4-7,8-11,12-15,…… 即使我们需要的数据只占一个字节,也是一次读取4个字节。 一个字节的数据不管地址是什么,都能通过一次访存读取出来。 而如果要读取的数据是一个字节以上,比如两个字节, 如果该数据的内存地址是0x03,则需要两次才能读取该数据, 第一次读0x00-0x03,第二次读0x04-0x07。 这个数据就跨越了访存边界。而相对CPU的运算来说,访存是非常慢的,所以要尽量减少访存次数。 为了减少跨越访存边界的数据引起的访存开销, 所以编译器会进行内存对齐,即把变量的地址做一些偏移, 目的是一次访存就读出数据,不然的话也要以尽可能少地访存次数读出数据。如上一个例子中那样,整型成员i的地址做4个字节的偏移, 而Sample对象的地址也会做4字节边界的对齐, 这样i的地址始终是4的倍数,从而使得i不跨越访存边界, 能一次读出它的值。

typedef struct{
    char a;
    char b;
    int i;
} Sample1;

Sample1占多少空间呢?仍然是8个字节。 a在第0个字节,b在第1个字节,i占4-7字节。 这是内存对齐的原则,占用尽量少的内存。 如果在b之后,还有char类型的成员c和d,同样是占8个字节。 a,b,c,d在0-3字节。

引用

原文地址:https://www.cnblogs.com/jaychan/p/11261404.html

时间: 2024-10-29 19:07:31

深入剖析PHP7内核源码(二)- PHP变量容器的相关文章

(升级版)Spark从入门到精通(Scala编程、案例实战、高级特性、Spark内核源码剖析、Hadoop高端)

本课程主要讲解目前大数据领域最热门.最火爆.最有前景的技术——Spark.在本课程中,会从浅入深,基于大量案例实战,深度剖析和讲解Spark,并且会包含完全从企业真实复杂业务需求中抽取出的案例实战.课程会涵盖Scala编程详解.Spark核心编程.Spark SQL和Spark Streaming.Spark内核以及源码剖析.性能调优.企业级案例实战等部分.完全从零起步,让学员可以一站式精通Spark企业级大数据开发,提升自己的职场竞争力,实现更好的升职或者跳槽,或者从j2ee等传统软件开发工程

Spark2.0从入门到精通:Scala编程、大数据开发、上百个实战案例、内核源码深度剖析视频教程

38套大数据,云计算,架构,数据分析师,Hadoop,Spark,Storm,Kafka,人工智能,机器学习,深度学习,项目实战视频教程 视频课程包含: 38套大数据和人工智能精品高级课包含:大数据,云计算,架构,数据挖掘实战,实时推荐系统实战,电视收视率项目实战,实时流统计项目实战,离线电商分析项目实战,Spark大型项目实战用户分析,智能客户系统项目实战,Linux基础,Hadoop,Spark,Storm,Docker,Mapreduce,Kafka,Flume,OpenStack,Hiv

Linux tcp被动打开内核源码分析

[我是从2个角度来看,其实所谓2个角度,是发现我分析源码时,分析重复了,写了2个分析报告,所以现在都贴出来.] [如果你是想看看,了解一下内核tcp被动打开时如何实现的话,我觉得还是看看概念就可以了,因为即使看了源码,过一个个礼拜你就忘了,如果是你正在修改协议栈,为不知道流程而发愁,那么希望你能看看源码以及注释,希望你给你帮助.] 概念: tcp被动打开,前提是你listen,这个被动打开的前提.你listen过后,其实创建了一个监听套接字,专门负责监听,不会负责传输数据. 当第一个syn包到达

Linux内核源码分析方法

  一.内核源码之我见 Linux内核代码的庞大令不少人“望而生畏”,也正因为如此,使得人们对Linux的了解仅处于泛泛的层次.如果想透析Linux,深入操作系统的本质,阅读内核源码是最有效的途径.我们都知道,想成为优秀的程序员,需要大量的实践和代码的编写.编程固然重要,但是往往只编程的人很容易把自己局限在自己的知识领域内.如果要扩展自己知识的广度,我们需要多接触其他人编写的代码,尤其是水平比我们更高的人编写的代码.通过这种途径,我们可以跳出自己知识圈的束缚,进入他人的知识圈,了解更多甚至我们一

编译Android系统源码和内核源码

好长时间没有写blog了,之所以没有写,主要还是工作上的事,发现最近的脑子不够用了,今天写点什么呢?就把我之前编译Android系统源码和内核源码的过程记录一下,因为这个过程真的是受益匪浅,看重的不是结果,主要是过程,在这个过程中,我感觉最大的收获就是学习的耐心和毅力,因为在这个过程中那个问题就像是雨点似的天天打在你的脸上,虽然现在网上有很多文章介绍怎么去操作,但是我说句真心话,那些只能提供参考,因为你的工作环境毕竟和他不一样,所以等你按照他的步骤去操作的时候还是会遇到很多问题,当然我写这篇文章

linux内核源码注解

轻松学习Linux操作系统内核源码的方法 针对好多Linux 爱好者对内核很有兴趣却无从下口,本文旨在介绍一种解读linux内核源码的入门方法,而不是解说linux复杂的内核机制:一.核心源程序的文件组织:1.Linux核心源程序通常都安装在/usr/src/linux下,而且它有一个非常简单的编号约定:任何偶数的核心(例如2.0.30)都是一个稳定地发行的核心,而任何奇数的核心(例如2.1.42)都是一个开发中的核心. 本文基于稳定的2.2.5源代码,第二部分的实现平台为 RedHat Lin

Android源码和内核源码的下载,编译和执行

笔者依据罗升阳老师的<Android 系统源码情景分析>一书,尝试下载,编译和执行Android源码和内核源码.但可能是软件源"被墙"或版本号更新的原因.期间遇到诸多问题.笔者求助于强大的度娘和谷歌.最终编译成功,现将改动的步骤记录例如以下: 一.下载相关的工具包和依赖包 1.git工具的下载.不多说 2.Java SDK的下载(參考网址:http://blog.csdn.net/zjclugger/article/details/11762085) 此处建议下载jdk1.

【Android 系统开发】 编译 Android 系统 u-boot 内核 源码 并烧写到 OK-6410A 开发板上

博客地址 : http://blog.csdn.net/shulianghan/article/details/40299813  本篇文章中用到的工具源码下载 : -- ok-6410A 附带的 Android 光盘 下载地址 : http://pan.baidu.com/share/link?shareid=3662728609&uk=2754759285 ; -- 光盘所含内容 : Android 引导 u-boot 源码, Android 内核 源码, Android 系统源码, 交叉编

FW 编译Android系统源码和内核源码

编译Android系统源码和内核源码 分类: Android2014-07-21 20:58 7287人阅读 评论(28) 收藏 举报 好长时间没有写blog了,之所以没有写,主要还是工作上的事,发现最近的脑子不够用了,今天写点什么呢?就把我之前编译Android系统源码和内核源码的过程记录一下,因为这个过程真的是受益匪浅,看重的不是结果,主要是过程,在这个过程中,我感觉最大的收获就是学习的耐心和毅力,因为在这个过程中那个问题就像是雨点似的天天打在你的脸上,虽然现在网上有很多文章介绍怎么去操作,