PHP7源码分析之字符串简单分析

1,009 阅读3分钟

php5.x的字符串结构:

struct _zval_struct {
	/* Variable information */
	struct {
		char *val;
		int len;
	} str;
	zend_uint refcount__gc;
	zend_uchar type;	/* active type */
	zend_uchar is_ref__gc;
};

可以看到php5的字符串是直接以结构体形式放在zval结构体中的。

再来看下php7的字符串结构:

struct _zend_string {
	zend_refcounted_h gc;
	zend_ulong        h;                /* hash value */
	size_t            len;
	char              val[1];
};

php7中是单独增加了一个zend_string结构体来表示字符串,也就是和zval分离了。

  • gc字段 gc字段是zend_refcounted_h结构体,其中的refcount表示引用计数
  • h字段 该字段表示缓存的字符串的哈希值,该字段仅在字符串被当作数组时才会使用到,且同一个字符串被当作key使用时不会重复计算其哈希值
  • val字段 val字段用到了柔性数组,当结构体中仅有一个变长字段时且为最后一个字段时,就可以使用柔性数组的表示方式。

柔性数组有什么好处呢?
在php7中,字符串初始化内存时的函数是zend_string_alloc,具体如下:

static zend_always_inline zend_string *zend_string_alloc(size_t len, int persistent)
{
	zend_string *ret = (zend_string *)pemalloc(ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(len)), persistent);

	GC_SET_REFCOUNT(ret, 1);
	GC_TYPE_INFO(ret) = IS_STRING | ((persistent ? IS_STR_PERSISTENT : 0) << GC_FLAGS_SHIFT);
	ZSTR_H(ret) = 0;
	ZSTR_LEN(ret) = len;
	return ret;
}

可以看到,在分配内存时,用到了_ZSTR_STRUCT_SIZE(len),然后跟踪进去可以找到#define _ZSTR_STRUCT_SIZE(len) (_ZSTR_HEADER_SIZE + len + 1),这个_ZSTR_HEADER_SIZE其实是#define _ZSTR_HEADER_SIZE XtOffsetOf(zend_string, val),其实到这里就可以看出,他分配的内存空间其实是zend_string结构体到柔性数组的长度加上len再加上1,为什么加1呢,因为还有个结束符“\0”。

而至此可以看出,柔性数组val其实占据了结构体末尾连续的一块内存,可以用于存储不定长度的字符串值。这样,zend_string的val就和其他字段一起存储在了同一块连续的内存块中,再分配、释放内存的时候可以把struct当作整体来处理。

php5中则将字符串值单独出来用指针表示,存储在另一块内存中,这就表示结构体和字符串值是分散的两块内存,在读取到zval结构体所在内存后,还需要再到零一块内存中才能读取字符串的值。

php7则只需要一次内存读取即可,节省了一次内存读写:

那为什么定义val时使用的时val[1]呢,不能定义成val[0],val[]吗?
因为val[]、val[0]是C99标准中才合法的,也就是说以前的版本是没有这么个表示方式的,而val[1]在C99标准和老版本中都是有的,所以为了兼容不同版本的C编译器,使用了val[1]来表示柔性数组。同时,val[1]仅表示占位的意思,并不占用实际内存。

  • len字段 而最后这个len字段,php7中是8字节,php5中则是4个字节,很明显,php7能够表示的字符串长度要比php5大得多,php5则可以表示(2的31次方-1)的长度,而php7为(2的63次方-1)的长度,明显提升了很多。
    使用len字段不仅可以不用每次读取字符串长度时计算字符串长度,同时也可以保证字符串操作的二进制安全,因为是按照长度来读取的。