深入剖析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)
- $a 赋值字符串,zend_string_1 的引用计数记为1。
- 把$a的引用赋值给$b,zend_string_1 结构的引用计数不变,产生了一个中间结构体zend_reference_1,该结构体的引用计数为2。
- $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字节。
引用
- 深入理解PHP7内核之zval http://www.laruence.com/2018/04/08/3170.html
- C语言的内存对齐 https://www.cnblogs.com/jiqingwu/p/4043338.html
- php7-internal https://github.com/pangudashu/php7-internal/blob/master/2/zval.md
- 《PHP7 底层设计与源码实现》 陈雷等
深入剖析PHP7内核源码(二)- PHP变量容器的更多相关文章
- 深入剖析PHP7内核源码(一)- PHP架构与生命周期
PHP7 为什么这么快? 全新的zval 更节约的空间,栈上分配内存 zend_string 存储字符串的Hash值,数组查询的时候不需要进行Hash计算 在HashTable桶内直接存数据,减少了内 ...
- 全方位深度剖析PHP7底层源码(已完结)
第1章 课程介绍本章主要介绍课程要讲的知识点,以及课程要求等. 第2章 PHP7的新特性本章主要介绍PHP7的新特性,做基准测试,与PHP5对比验证PHP7的性能提升程度,引出对PHP7源码学习的必要 ...
- spring源码 — 二、从容器中获取Bean
getBean 上一节中说明了容器的初始化,也就是把Bean的定义GenericBeanDefinition放到了容器中,但是并没有初始化这些Bean.那么Bean什么时候会初始化呢? 在程序第一个主 ...
- (升级版)Spark从入门到精通(Scala编程、案例实战、高级特性、Spark内核源码剖析、Hadoop高端)
本课程主要讲解目前大数据领域最热门.最火爆.最有前景的技术——Spark.在本课程中,会从浅入深,基于大量案例实战,深度剖析和讲解Spark,并且会包含完全从企业真实复杂业务需求中抽取出的案例实战.课 ...
- linux0.11内核源码剖析:第一篇 内存管理、memory.c【转】
转自:http://www.cnblogs.com/v-July-v/archive/2011/01/06/1983695.html linux0.11内核源码剖析第一篇:memory.c July ...
- v77.01 鸿蒙内核源码分析(消息封装篇) | 剖析LiteIpc(上)进程通讯内容 | 新的一年祝大家生龙活虎 虎虎生威
百篇博客分析|本篇为:(消息封装篇) | 剖析LiteIpc进程通讯内容 进程通讯相关篇为: v26.08 鸿蒙内核源码分析(自旋锁) | 当立贞节牌坊的好同志 v27.05 鸿蒙内核源码分析(互斥锁 ...
- v78.01 鸿蒙内核源码分析(消息映射篇) | 剖析LiteIpc(下)进程通讯机制 | 百篇博客分析OpenHarmony源码
百篇博客分析|本篇为:(消息映射篇) | 剖析LiteIpc(下)进程通讯机制 进程通讯相关篇为: v26.08 鸿蒙内核源码分析(自旋锁) | 当立贞节牌坊的好同志 v27.05 鸿蒙内核源码分析( ...
- 《Unix内核源码剖析》
<Unix内核源码剖析> 基本信息 作者: (日)青柳隆宏 译者: 殷中翔 丛书名: 图灵程序设计丛书 出版社:人民邮电出版社 ISBN:9787115345219 上架时间:2014-2 ...
- Linux内核驱动学习(二)添加自定义菜单到内核源码menuconfig
文章目录 目标 drivers/Kconfig demo下的Kconfig 和 Makefile Kconfig Makefile demo_gpio.c 目标 Kernel:Linux 4.4 我编 ...
随机推荐
- 如何启用linux的路由转发功能
如何使用iptables的NAT功能把红帽企业版Linux作为一台路由器使用? 方法: 提示: 以下方法只适用于红帽企业版Linux 3 以上. 1.打开包转发功能: echo "1&quo ...
- AbstractList
概述 此类提供 List 接口的骨干实现,以最大限度地减少实现“随机访问”数据存储(如数组)支持的该接口所需的工作.对于连续的访问数据(如链表),应优先使用 AbstractSequentialLis ...
- python传递参数
1.脚本 # -*- coding: utf-8 -*- from sys import argvscript, first,second = argv #将命令中输入的参数解包后传递给左边 age ...
- 【译】在 Linux 上不安装 Mono 构建 .NET Framework 类库
在这篇文章中,我展示了如何在Linux上构建针对.NET Framework版本的.NET项目,而不使用Mono.通用使用微软新发布的 Mocrosoft.NETFramework.Reference ...
- WTM 构建DotNetCore开源生态,坐而论道不如起而行之
作为一个8岁开始学习编程,至今40岁的老程序员,这辈子使用过无数种语言,从basic开始,到pascal, C, C++,到后来的 java, c#,perl,php,再到现在流行的python. 小 ...
- WIN10安装VC6.0无法使用的解决办法
WIN10安装VC6.0无法使用的解决办法 VC6.0确实已经太老了 VC6.0实在是很久以前的开发工具了,现在的win10已经对该软件不兼容,但是为了能使抱着怀旧情节的初学者们能像教科书或老前辈们一 ...
- 使用Typora编写博客并发布
前言 用CSDN写了一段时间,广告漫天飞舞.... 于是在博客园申请了一个账号,然后看见markdown编辑页面的第一眼: 再见^_^ 搜索一波,凭着博客园强大的生态,30多万的用户,第三方的支持应接 ...
- php 中session_set_cookie_params 和 setcookie 函数的区别与用法
session_set_cookie_params() 函数不管刷不刷新页面,都不会改变cookie的过期时间, 但setcookie() 函数页面每刷新一次,cookie 的过期时间就会刷新一次. ...
- 安利一个免费下载VIP文档神器
今天安利给大伙一个非非非常好用的可以免费下载VIP文档的下载神器------冰点文库下载器,用过的人都说好.操作简单,小巧轻便,完全免费.支持百度.豆丁.畅享.mbalib.hp009.max.boo ...
- 3.php基础(控制语句,函数,数组遍历)
if条件判断语句 结构一:只判断true,不管false 结构二:既判断true,也判断false(二选一) 结构三:多条件判断 switch多分支结构 Switch语法结构说明: l Switch的 ...