PHP内核–数据类型
简介:在PHP语言中一共拥有整形、浮点型、字符串、引用、资源、实例和数组,一共7种数据类型。其中数组实现其他语言(如python)中的元组、列表和字典等功能。本文将对PHP内核中对数据类型的一些简单内容进行讲解。
PHP内核中保存数据的结构体
结构体zval是保存PHP代码中变量的值,而zend_value则是保存了实际的值。结构体定义在zend_types.h头文件中。
typedef union _zend_value {
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;
} zend_value;
struct _zval_struct {
zend_value value; /* value */
union {
uint32_t type_info;
struct {
ZEND_ENDIAN_LOHI_3(
zend_uchar type, /* active type */
zend_uchar type_flags,
union {
uint16_t extra; /* not further specified */
} u)
} v;
} u1;
union {
uint32_t next; /* hash collision chain */
uint32_t cache_slot; /* cache slot (for RECV_INIT) */
uint32_t opline_num; /* opline number (for FAST_CALL) */
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 */
uint32_t property_guard; /* single property guard */
uint32_t constant_flags; /* constant flags */
uint32_t extra; /* not further specified */
} u2;
};
zval
struct zval一共有三个成员变量(value,u1,u2),大小为16Bytes。
value保存了变量中实际的值。u1用于标识数据类型u2用于额外的操作需要(如用于hash冲突链表的next,用于函数参数的初始化cache_slot,快速调用FAST_CALL的opline_num,用于抽象语法树节点行号的lineno,用于执行的参数个数,用于foreach的位置,用于foreach的迭代器索引,单一类型保护,常量标识)
zend_value是一个共同体,大小是8Bytes,可以保存整形,浮点型数据。还有引用计数,字符串、数组、实例、资源、引用等指针。还包括内核自用的类结构体和函数结构体。
1.整形(integer)
在PHP内核中整形保存在zend_value中的zend_long,为有符号的64位数字。对于整形来说没有引用计数,在内核编译的过程中和执行过程中,整形可以直接释放。
2.浮点型(float|double)
浮点型根据服务器所采用的标准(IEEE 754标准)不同有不同的取值。与整形不同的是,浮点型采用的是二进制科学计数法进行表示((+0.875)10=(+1.11∗2−1)2(+0.875)_{10} = (+1.11 * 2 ^{-1})_{2}(+0.875)10=(+1.11∗2−1)2)。
一个浮点数(Value)可以表示为:
Value = sign * exponent * fraction
sign是符号位,表示浮点数的正负,占一位二进制位。exponent是二进制科学计数法的指数位(俗称指数域)。在单精度float标准下占8位二进制位,在双精度浮点数在占11位二进制位。以指数偏移量的形式存储,固定偏移值为2e−1−12^{e-1}-12e−1−1,在单精度下为28−1−1=1272^{8-1}-1 = 12728−1−1=127。如指数实际值为12(10)12_{(10)}12(10),则在单精度浮点数中的指数域偏码值为12+127=13912 + 127 = 13912+127=139fraction是浮点数中的尾数。分数 (fraction) 部分最高有效位(即整数字)是111。如上面的例子,在浮点数的尾数域中只保存小数点之后的二进制位。
所以上面的1.11∗2−11.11 * 2^{-1}1.11∗2−1可以实际表示为:
0∣01111110∣110000000000000000000000|01111110|110000000000000000000000∣01111110∣11000000000000000000000
符号域为000表示正数
指数域为011111100111111001111110 = 126(10)126_{(10)}126(10),实际的值为126−127=−1126 - 127 = -1126−127=−1
尾数域为110000000000000000000001100000000000000000000011000000000000000000000,实际可以表示为1.110000000000000000000001.110000000000000000000001.11000000000000000000000


以上是十进制的小数转换成二进制的科学计数法的计算过程。最后都可以用1.p∗2(2)e1.p*2^{e}_{(2)}1.p∗2(2)e来表示。
但是不可避免的,对于浮点数的存储位有限,对于有无限位数尾数的情况,必定会产生一定精度的损失。
浮点数的有效位
单精度和双精浮点数的有效数字分别是有存储的23位和52个位,加上最左边没有存储的第1个位,即是24和53个位。
log224=7.22log2^{24} = 7.22log224=7.22
log253=15.95log2^{53} = 15.95log253=15.95
由以上计算,单精和双精浮点数可以保证7位和15位十进制有效数字。
十进制精度
9位十进制浮点数才能保证近似转化为32比特浮点再近似转化回9位十进制浮点后保持值不变,双精浮点数则为17位。
3.字符串(string)
字符串结构体
struct _zend_string {
zend_refcounted_h gc;
zend_ulong h; /* hash value */
size_t len;
char val[1];
};
成员变量:
- gc:引用计数,保存了字符串被引用的次数和字符串本身的数据类型type。
- h:hash值,通过time33算法计算出的字符串的hash值,大部分用于数组操作。
- len:字符串长度,在字符串操作中不需要要再重复计算字符串的长度。
- val[1]:字符串数组占位符,用来表示字符串开始的位置。
在内核初始化时,会生成一份hashTable用来保存内部字符串。内部字符串是在程序编译时可以确认的字符串,如类名、函数名、变量名和常量字符串等。在初始化时就已经加入了1-256的数字字符串。
<?php
class test{
public $info;
public function hello(){
}
}
$a = new test();
$b = "bye";
$c = $b."!";
$d = "test";
>
如上面这段代码,在内核编译时,test、hello、a、b、c、bye、!这些字符串都是可以直接确定,而$c所指向的字符串bye!则需要在执行时才能确认。
zend内核将这两种情况产生的字符串分开处理,内部字符串在编译时会加入到全局字符串表interned_strings中,相同的字符串指向相同的内存地址,如类test的类名与变量$d所指向的字符串是相同,所以他们指向的是同一个内存地址。
内部字符串在程序结束才会释放。
json_encode(str,option,depth)
在option的值为JSON_UNESCAPED_UNICODE时才会解析unicode字符,不然都是暴露出除ASCII码外的unicode字符,且该范围只包括基本平面,不包括增补平面。
utf8和UTF-8
uft8是MYSQL里的字符集,表示的是使用三个字节UTF-8进行编码的unicode字符集,只能表示基本平面,即65536个字符。包括几乎所有国家和地区的文字,不包括大部分制表符、特殊符号和EMOJI表情包。
UTF-8是UNICODE字符集的编码方式,unicode字符分为0-16一共17个平面,每个平面包含65536个编码,每个字符对应一个编码。平面0称为字符集的基本面,保存了几乎所有文字。而增补平面(1-16)通常用来标识制表符、特殊符号和EMOJI表情包。
最近发布的Unicode 14.0,共计144,697个字符。
utf8和utf8mb4
utf8和utf8mb4都是MYSQL的字符集。utf8mb4支持四字节UTF-8编码的unicode字符,所以utf8mb4支持增补平面。但utf8mb4是否支持对应的unicode字符由机器所使用的unicode版本决定。
在MYSQL5.6版本中,如果sql语句包含不支持的unicode字符(如emoji表情,特殊字符等),会在该字符处截断sql语句。而在MYSQL5.7及以上版本中,会报ERROR 1366(HY000):INCORRECT STRING VALUE :。如emoji表情🈶,用unicode来表示为U+1F236,如果MYSQL所在的机器的unicode版本不支持这个emoji字符,则会报错。
二进制安全
在C语言中,字符串是以’\0’结尾。所以在一串字符串中如果存在’\0’,在对这个字符串计算长度时会以第一个’\0’为界限。
如字符串’abc\0abc\0’,字符串的长度为3而不是8。这样在字符串的操作(如拼接、查找)中都是不安全的。
而在结构体中记录字符串的长度可以避免这个问题。
数组(hashTable)
typedef struct _Bucket {
zval val;
zend_ulong h; /* hash value (or numeric index) */
zend_string *key; /* string key or NULL for numerics */
} Bucket;
typedef struct _zend_array HashTable;
struct _zend_array {
zend_refcounted_h gc;
union {
struct {
ZEND_ENDIAN_LOHI_4(
zend_uchar flags,
zend_uchar _unused,
zend_uchar nIteratorsCount,
zend_uchar _unused2)
} v;
uint32_t flags;
} u;
uint32_t nTableMask;
union {
uint32_t *arHash; /* hash table (allocated above this pointer) */
Bucket *arData; /* array of hash buckets */
zval *arPacked; /* packed array of zvals */
};
uint32_t nNumUsed;
uint32_t nNumOfElements;
uint32_t nTableSize;
uint32_t nInternalPointer;
zend_long nNextFreeElement;
dtor_func_t pDestructor;
};
PHP用数组实现了其他语言中(元组,列表,字典等)数据结构的功能。尤其是在PHP8.0中,ZEND内核为数组增加了一个flag成员变量用来标识数组是否需要通过hash取值。
PHP数组的一个重要的特点是,在数据存储上是连续。对于hashTable来说,如何解决hash冲突是非常关键的,我们不仅需要在hash算法中,尽量使索引具有足够的离散性,还需要对产生hash冲突的数据进行处理。通常的解决办法是通脱开放地址和拉链式。拉链式是解决冲突的比较好的方法,但是会造成数据存储上的不连续。而开放地址法虽然能够让数据存储在连续的内存上,但是相对拉链式会降低取值的效率。
PHP为了实现内存的连续采取了个折中的办法。通过额外的数组来保存拉链的起始索引。这样做的好处是内存释放的时候只要调用一次free就够了。而对应拉链式需要遍历每一个元素单独进行释放。
数组重构,当数组的元素满的时候(指的是占用了最后一个数组位置),会申请一份本身内存的两倍的内存用于数组的扩容并rehash。
实例(Object)
struct _zend_object {
zend_refcounted_h gc;
uint32_t handle; // TODO: may be removed ???
zend_class_entry *ce;
const zend_object_handlers *handlers;
HashTable *properties;
zval properties_table[1];
};
成员变量包括引用计数gc,类的编译结构ce,实例操作函数表(获取方法,获取类名等)handlers,类的成员变量数组properties。
序列化与反序列化
<?php
class a{
public $c = 12;
protected $e = 10;
private $d = 11;
public function test(){
echo "111";
}
public function __sleep(){
return ["c","e"];
}
}
$a = new a();
var_dump($a);
$b = serialize($a);
$c = unserialize($b);
var_dump($b);
var_dump($c);
$c->test();
serialize()函数将变量转化成字符串的形式。以字母开头来标识变量的类型,例如:N(表示变量是空的),R:(表示变量是引用类型),b:0(表示false),b:1(表示true),d:(表示double),s:(表示字符串变量),E:(表示枚举类),O:(表示实例);
在PHP7以上中,在序列化时会截断序列化字符串("O:1:"a":2:{s:1:"c";i:12;s:4:"),这是因为在类的编译结构中,存储private和protected变量时,会在变量名的头部插入"\000"。在反序列化时为空实例。可以通过__sleep()魔术方法返回公共变量来规避这个问题。字符串变为("O:1:"a":1:{s:4:"var1";i:12}"),unserialize()之后为正常的实例。
在PHP5.4则可以正常显示为("O:1:"a":2:{s:1:"c";i:12;s:4:"\000*\000e";i:10;}"),可以正常反序列化。
PHP内核数据类型详解
412

被折叠的 条评论
为什么被折叠?



