智能字符串
前文已经讲解了普通字符串结构体及对应操作函数的实现,但是,当 PHP 需要频繁对一个字符串进行扩容修改时,则会使用到智能字符串相关的结构体及对应的操作函数。例如,从 PHP 5.4.0 起,CLI SAPI 提供了一个内置的 Web 服务器,当遇到请求不存在的 PHP 文件时,会报错 404,而这个 404 页面的 HTML 源码就是通过调用智能字符串函数拼接而成的;再比如,我们常用到 PHP 的 var_export
函数,它需要拼接较长的字符串,这个也是通过调用智能字符串函拼接而成的。智能字符串函数的主要功能是对 zend_string
API 的一种补充,可以更高性能地实现字符串的扩容组装。
smart_str 对比 smart_string
在阐述智能字符串之前,先看看其实现所依赖的基本结构 smart_str
与 smart_string
结构体的异同。
-
两者都是智能字符串管理函数、宏实现依赖的基本结构体。
-
两者实现了相同功能的字符串管理宏。
-
smart_str
是 PHP 7 智能字符串管理函数、宏实现所依赖的基本结构体,而smart_string
是老版本 PHP 智能字符串管理函数、宏实现所依赖的基本结构体。 -
smart_str
依赖zend_string
结构体存储字符串的值,而smart_string
字符串的值存储直接使用char*
。
大部分的 PHP 7 源码调用基于 smart_str
结构体实现的智能字符串管理函数、宏,但也有少部分源码仍调用基于 smart_string
结构体实现的智能字符串管理函数、宏。
下面来看看这两个结构体的源码。
smart_str
结构体:
typedef struct {
zend_string * s ; /*字符串值存储在zend_string.val中*/
size_t a ; /*申请的内存空间总大小*/
} smart_str ;
struct _zend_string {
zend_refcounted_h gc; /*引用计数及字符串类别存储*/
zend_ulong h; /*哈希值*/
size_t len; /*已使用内存的字符串长度*/
char val[1]; /*字符串值的存储位置*/
};
smart_string
结构体:
typedef struct {
char * c ; /*字符串值的存储位置*/
size_t len ; /*已使用内存的字符串长度*/
size_t a ; /*申请的内存空间总大小*/
} smart_string ;
因为本书主要讲 PHP 7 的底层设计与实现,笔者在这里也主要分析基于 smart_str
结构体实现的智能字符串管理函数与宏。
智能字符串的具体实现
先看看智能字符串实现所依赖的 smart_str
结构体的每个字段对应的含义。
-
s
字段:字符串指针,指向的是zend_string
结构体,用于存储智能字符串的值及已使用空间大小等。 -
a
字段:智能字符串申请的内存空间总大小。看完智能字符串结构体的字段介绍,我们会发现其存储字符串所依赖的也是zend_string
,那相比普通的字符串扩容函数,为什么其性能会更高呢?结合示例代码一起来分析,代码如下。
示例1:使用普通字符串函数,zend_string_extend
实现字符串扩容追加:
zend_string *FOO , *bar , *foobar ;
FOO = zend_string_init("foo" , strlen("foo"), 0);
bar = zend_string_init("bar" , strlen("bar"), 0);
foobar = zend_string_copy(FOO);
/* 调用zend_string_extend函数扩容,每次都需申请内存 */
foobar = zend_string_extend(foobar , strlen("foobar"), 0);
/* 在重新分配足够存储bar之后连接"bar" */
memcpy(ZSTR_VAL(foobar) + ZSTR_LEN(FOO), ZSTR_VAL(bar), ZSTR_LEN(bar))
前文已经介绍过 zend_string_extend
函数,通过 zend_string_extend
函数实现字符串的扩容,最少涉及一次内存分配(申请新的大内存,可能还涉及释放一次老内存)。
示例2:使用智能字符串函数,smart_str_appendl_ex
实现字符串扩容追加:
smart_str *foobar;
/* 申请内存,往智能字符串 foobar 里面写入 "foo" */
smart_str_appendl_ex(foobar , "foo" , strlen("foo") , 0);
/* 往智能字符串 foobar 里面追加 "bar" */
smart_str_appendl_ex(foobar , "bar" , strlen("bar") , 0);
/* smart_str_appendl_ex 函数实现 */
smart_str_appendl_ex(smart_str *dest, const char *str, size_t len, zend_bool persistent)
{
/* 调用 smart_str_alloc 函数申请内存 */
size_t new_len = smart_str_alloc(dest, len, persistent);
/* 重新分配足够存储 "bar" 的内存后连接字符串 "bar" */
memcpy(ZSTR_VAL(dest->s) + ZSTR_LEN(dest->s), str, len);
ZSTR_LEN(dest->s) = new_len;
}
结合 示例 1 与 示例 2 的代码,会发现字符串扩容追加的步骤区别不大,而 smart_str_appendl_ex
函数在字符串扩容追加方面性能高的原因在于,扩容时申请内存的过程优化了。申请内存时,会先申请一块较大的连续内存(申请内存具体大小时的逻辑如图 4-4 所示),并把申请的总长度值写入 smart_str.a
字段中,把已使用的长度写入 smart_str.s.len
字段中。当智能字符串需要追加新字符串时,直接检查剩余内存块长度够不够,够了,则直接追加,不够,则重新申请一块更大的内存。通过空间换时间的做法,避免了每次追加都得去申请或释放内存。

图4-4中 |
对于 示例2 中的变量 foobar
,在追加字符串 “foo”
与 “bar”
后的内存分布如图4-5所示。

从图 4-5 中可以看出,类型为智能字符串的变量 foobar
,可存储字符串值的内存总长度为 232 字节(zend_string
结构体已占用 24 字节),已使用的为 6 字节,追加的字符串大小不大于等于 226 字节,就不会触发申请新内存及释放老内存的操作,所以相比 zend_string_extend
函数实现的字符串追加操作,性能更优。
通过上述两个示例的对比,可以发现 smart_str
实现字符串的追加扩容的优化点有:
-
封装了 API,在使用上可以简单地调用 API 方法,从而实现各种类别的字符追加;
-
优化了内存申请的环节,不需要频繁地申请及释放内存,可以更高性能地完成字符串的追加扩容。
smart_str API
前面已经讲述了智能字符串结构体,以及它与普通字符串相比在实现字符串扩容这件事上的优点,下面主要介绍智能字符串的几个追加函数,因为和前一小节讲述的 smart_str_appendl_ex
方法类似,笔者在这里也只是做一个简单的函数功能介绍,不再具体分析源码,相关函数如下:
smart_str_appendl_ex((dest), (src), (len), 0) /* 往smart_str追加char*字符串 */
smart_str_appendc_ex((dest), (c), 0) /* 往smart_str追加一个char字符 */
smart_str_append_ex((dest), (src), 0) /* 往smart_str追加zend_string类型的字符串 */
smart_str_append_smart_str_ex((dest), (src), 0) /* 往smart_str追加smart_str */
smart_str_setl((dest), (src), strlen(src)); /* 往smart_str覆盖追加char*字符串 */
smart_str_append_long_ex((dest), (val), 0) /* 往smart_str追加int32类型的num */
smart_str_append_unsigned_ex((dest), (val), 0) /* 往smart_str追加uint32_t类型的num */
smart_str_erealloc(smart_str *str, size_t len); /* 通过PHP内存管理方式申请内存 */
smart_str_realloc(smart_str *str, size_t len); /* 通过C的realloc或malloc申请内存 */
smart_str_append_escaped(smart_str *str, const char *s, size_t l); /* 将经过转义的字符串追加到 smart_str 对象中 */
smart_str_alloc(smart_str *str, size_t len, zend_bool persistent) { /* 申请内存 */
smart_str_free(smart_str *str) { /* 释放 */
smart_str_0(smart_str *str) { /* 尾部追加\0,可以使用 C 函数处理*/