智能字符串

前文已经讲解了普通字符串结构体及对应操作函数的实现,但是,当 PHP 需要频繁对一个字符串进行扩容修改时,则会使用到智能字符串相关的结构体及对应的操作函数。例如,从 PHP 5.4.0 起,CLI SAPI 提供了一个内置的 Web 服务器,当遇到请求不存在的 PHP 文件时,会报错 404,而这个 404 页面的 HTML 源码就是通过调用智能字符串函数拼接而成的;再比如,我们常用到 PHP 的 var_export 函数,它需要拼接较长的字符串,这个也是通过调用智能字符串函拼接而成的。智能字符串函数的主要功能是对 zend_string API 的一种补充,可以更高性能地实现字符串的扩容组装。

smart_str 对比 smart_string

在阐述智能字符串之前,先看看其实现所依赖的基本结构 smart_strsmart_string 结构体的异同。

  1. 两者都是智能字符串管理函数、宏实现依赖的基本结构体。

  2. 两者实现了相同功能的字符串管理宏。

  3. smart_str 是 PHP 7 智能字符串管理函数、宏实现所依赖的基本结构体,而 smart_string 是老版本 PHP 智能字符串管理函数、宏实现所依赖的基本结构体。

  4. 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 结构体的每个字段对应的含义。

  1. s 字段:字符串指针,指向的是 zend_string 结构体,用于存储智能字符串的值及已使用空间大小等。

  2. 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 字段中。当智能字符串需要追加新字符串时,直接检查剩余内存块长度够不够,够了,则直接追加,不够,则重新申请一块更大的内存。通过空间换时间的做法,避免了每次追加都得去申请或释放内存。

image 2024 06 08 10 02 07 296
Figure 1. 图4-4 智能字符串内存申请——确定申请内存具体大小的逻辑

图4-4中 len 的定义为:len = string.len + new_str_len + string_struct_len + 1,即原有字符串大小+新追加字符串大小+string结构体大小(24)+字符“\0”大小(1)。

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

image 2024 06 08 10 03 22 698
Figure 2. 图4-5 智能字符串内存分布

从图 4-5 中可以看出,类型为智能字符串的变量 foobar,可存储字符串值的内存总长度为 232 字节(zend_string 结构体已占用 24 字节),已使用的为 6 字节,追加的字符串大小不大于等于 226 字节,就不会触发申请新内存及释放老内存的操作,所以相比 zend_string_extend 函数实现的字符串追加操作,性能更优。

通过上述两个示例的对比,可以发现 smart_str 实现字符串的追加扩容的优化点有:

  1. 封装了 API,在使用上可以简单地调用 API 方法,从而实现各种类别的字符追加;

  2. 优化了内存申请的环节,不需要频繁地申请及释放内存,可以更高性能地完成字符串的追加扩容。

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 函数处理*/