最近对 PHP 语言的标准库实现的一些优化

最近我正在读 PHP 内核的标准库源码。好,我们接下来看看我在这次旅程中顺便做了什么样的改动。

利用倍数复制法优化 str_pad 和 mb_str_pad

str_pad 函数可以将字符串用指定字符填充至所需要的长度。其操作类似于 python 中的 ljust 和 rjust。

1
str_pad(string $string, int $length, string $pad_string = " ", int $pad_type = STR_PAD_RIGHT): string

如 str_pad("abc", 8, "xy", STR_PAD_RIGHT); 这一行代码,意思就是将 abc 这个字符串用 xy 从右边填充到长度 8,输出 abcxyxyx。

其在内核中主要以以下代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static void php_str_pad_fill(zend_string *result, size_t pad_chars, const char *pad_str, size_t pad_str_len) {
char *p = ZSTR_VAL(result) + ZSTR_LEN(result);

if (pad_str_len == 1) {
memset(p, pad_str[0], pad_chars);
ZSTR_LEN(result) += pad_chars;
return;
}

const char *end = p + pad_chars;
while (p + pad_str_len <= end) {
p = zend_mempcpy(p, pad_str, pad_str_len);
}

if (p < end) {
memcpy(p, pad_str, end - p);
}

ZSTR_LEN(result) += pad_chars;
}

其中 result 是一个指向字符串的指针,可以当成是返回时用的缓冲区;pad_chars 代表了总共需要填充多少个字节;pad_str 是指向填充时使用的字符串的常量指针;pad_str_len 表示前者的长度。

以 str_pad("abc", 8, "xy", STR_PAD_RIGHT); 为例,上述函数的变量的值就是:pad_chars 是 5,pad_str 是 “xy”,pad_str_len 是 2。当这个函数返回后,就会得到 xyxyx(这时 result 的值就是 xyxyx),我们再把 xyxyx 和原始字符串 abc 拼在一起,就得到了正确的返回值 abcxyxyx。

简单易懂不是吗(^~^)

我们来看看这段代码的逻辑。首先是一个快速路径优化(fast-path optimization)

1
2
3
4
5
if (pad_str_len == 1) {
memset(p, pad_str[0], pad_chars);
ZSTR_LEN(result) += pad_chars;
return;
}

如果填充使用的字节长度是 1,就直接复制需要数量的该字符,直接返回就好。这时候就有人要问了:不应该所有的都是这样吗?为什么只有 1 可以呢?

因为,填充时使用的字符串的长度不一定是待填充长度的整数倍。只有在填充时使用的字符串长度是 1 的时候,才能保证前面的条件成立。试想我们用 abc 填充 7 个字符,我们就不能直接分配 7 个字节这么多内存再直接填充了,因为 abc 的长度是 3,我们最后应该填充的是 abcabcab 而不是 abcabcabc,直接复制三份 abc 会导致内存的越界写。我们来看 php 内核是怎么处理这种大多数情况的:

1
2
3
4
5
6
7
8
9
10
const char *end = p + pad_chars;
while (p + pad_str_len <= end) {
p = zend_mempcpy(p, pad_str, pad_str_len);
}

if (p < end) {
memcpy(p, pad_str, end - p);
}

ZSTR_LEN(result) += pad_chars;

简单粗暴啊。我们直接开一个循环。当剩余的长度大于或等于待填充字符串的长度时,我们循环向缓冲区拷贝待填充字符串;当剩余的长度小于待填充字符串的长度时,我们就从待填充的字符串的尾部减去相应长度的字符,再拷贝进缓冲区。随后,返回缓冲区。

正在阅读这段代码的你,有什么优化的想法吗?

假设我们要用 ab 这个字符串填充 1 MiB 的长度,那我们要循环五十多万次。循环的时间复杂度 O(+∞)

开个玩笑,O(pad_chars/pad_str_len)。严格上来说我们要循环 pad_chars/pad_str_len 次。

这个时候就轮到我们大显身手了!

我们来想,假设我们用 ab 填充 1024 个字节。

第 1 次填充的时候:缓冲区是 ab
第 2 次填充的时候:缓冲区是 abab
第 3 次填充的时候:缓冲区是 ababab
第 4 次填充的时候:缓冲区是 abababab
第 n (n<=512) 次填充的时候:缓冲区是 ab 与 n 的积

注意到,这里可以用对数的方法进行优化。我们在每次拷贝时,不从待填充的字符串拷贝,而是将缓冲区本身拷贝到缓冲区里。

第 1 次填充的时候:缓冲区是 ab
第 2 次填充的时候:缓冲区是 abab
第 3 次填充的时候:缓冲区是 abababab
第 3 次填充的时候:缓冲区是 abababababababab
第 n (n<=10) 次填充的时候:缓冲区是 ab 的 n 次方

我们只需要检测缓冲区的长度是否小于剩余的长度即可。若不小于,继续循环填充;若小于,则再从缓冲区的开头复制剩余长度的字符即可。

倘若我们填充 1027 个长度的 ab。第 10 次循环结束后,缓冲区是:

ab * 1024

此时,我们计算剩余长度 1037 - 1024 = 3。从缓冲区开头复制 3 个字符,也就是 aba,拷贝到缓冲区末尾即可。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void php_str_pad_fill(zend_string *result, size_t pad_chars, const char *pad_str, size_t pad_str_len) {
char *p = ZSTR_VAL(result) + ZSTR_LEN(result);

if (pad_str_len == 1) {
memset(p, pad_str[0], pad_chars);
ZSTR_LEN(result) += pad_chars;
return;
}

const char *start = p;
const char *end = p + pad_chars;
size_t len = MIN(pad_str_len, pad_chars);
p = zend_mempcpy(p, pad_str, len);

/* Double the filled area on each iteration. */
while (p < end) {
len = MIN(p - start, end - p);
p = zend_mempcpy(p, start, len);
}

ZSTR_LEN(result) += pad_chars;
}

这样我们把循环次数降到了 log(pad_chars)/log2。假设我们要填充 1 MiB 的 ab,原来需要五十多万次内存操作,现在仅仅需要 20 次!新实现先写入一份模式(不足一份则只写所需部分),之后从已填区域复制 min(已填长度, 剩余长度) 字节,直到完成。最后一次复制可以自然覆盖不足整轮长度的尾部,左右填充的外层规则不变。

至此,观察到极大的性能提升。由于原本循环次数是 pad_chars/pad_str_len,现在变成了 log(pad_chars/pad_str_len)/log2,提升的程度随 pad_chars 正相关,随 pad_str_len 负相关。

代码 调用次数 优化前 优化后 单次耗时减少 速度倍数
str_pad("", 1024, "ab") 100,000 51.786 ms 7.368 ms 85.77% 7.03×
str_pad("", 1024*1024, "ab") 150 73.701 ms 3.326 ms 95.49% 22.16×
str_pad("", 1024*1024, "abcde") 150 35.517 ms 2.966 ms 91.65% 11.97×

同样的我们把相似的策略应用于 mb_str_pad 上。原代码:

1
2
3
for (size_t i = 0; i < full_left_pad_copies; i++, buffer += ZSTR_LEN(pad)) {
memcpy(buffer, ZSTR_VAL(pad), ZSTR_LEN(pad));
}

更改后采用指数的复制模式。另外注意到 mb_str_pad 并没有应用上面介绍的对填充字符长度等于一时的 fast-path optimization,这里顺便加上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static char *php_mb_str_pad_fill(char *buffer, const zend_string *pad, size_t pad_bytes)
{
if (pad_bytes == 0) {
return buffer;
}
if (ZSTR_LEN(pad) == 1) {
memset(buffer, ZSTR_VAL(pad)[0], pad_bytes);
return buffer + pad_bytes;
}

const char *start = buffer;
const char *end = buffer + pad_bytes;
buffer = zend_mempcpy(buffer, ZSTR_VAL(pad), ZSTR_LEN(pad));

/* Double the filled area on each iteration. */
while (buffer < end) {
size_t len = MIN(buffer - start, end - buffer);
buffer = zend_mempcpy(buffer, start, len);
}
return buffer;
}

随后调用即可。

1
buffer = php_mb_str_pad_fill(buffer, pad, full_left_pad_bytes);

对于 str_pad:PR
对于 mb_str_pad:PR

直接通过位运算生成三位八进制转义优化 addcslashes

请看 VCR:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
PHPAPI zend_string *php_addcslashes_str(const char *str, size_t len, const char *what, size_t wlength)
{
char flags[256];
char *target;
const char *source, *end;
char c;
size_t newlen;
zend_string *new_str = zend_string_safe_alloc(4, len, 0, 0);

php_charmask((const unsigned char *) what, wlength, flags);

for (source = str, end = source + len, target = ZSTR_VAL(new_str); source < end; source++) {
c = *source;
if (flags[(unsigned char)c]) {
if ((unsigned char) c < 32 || (unsigned char) c > 126) {
*target++ = '\\';
switch (c) {
case '\n': *target++ = 'n'; break;
case '\t': *target++ = 't'; break;
case '\r': *target++ = 'r'; break;
case '\a': *target++ = 'a'; break;
case '\v': *target++ = 'v'; break;
case '\b': *target++ = 'b'; break;
case '\f': *target++ = 'f'; break;
default: target += snprintf(target, 4, "%03o", (unsigned char) c);
}
continue;
}
*target++ = '\\';
}
*target++ = c;
}
*target = 0;
newlen = target - ZSTR_VAL(new_str);
if (newlen < len * 4) {
new_str = zend_string_truncate(new_str, newlen, 0);
}
return new_str;
}

注意到

1
default: target += snprintf(target, 4, "%03o", (unsigned char)

这一行很有意思。它的功能是输入一个字节 c(0~255),输出固定 3 位带前导 0 的八进制 ASCII 字符串,例如 c=5 → "005";c=0xFF → "377"。

考虑需要八进制转义的字节,其数值范围只有 0~255,格式固定为三位八进制数字。旧实现对每个这样的字节调用 snprintf(..., "%03o", ...),仍需经过通用格式解析和整数格式化。

这里直接给出我的新实现:

1
2
3
4
5
default:
/* Write the byte as three octal digits, including leading zeros. */
*target++ = ((unsigned char) c >> 6) + '0';
*target++ = (((unsigned char) c >> 3) & 7) + '0';
*target++ = ((unsigned char) c & 7) + '0';

用了一点点小 hack,不要在意。这才是 C 语言该有的样子魂淡。总之我们通过位移和掩码的的手段分别提取出高,中,低三组位。取位前转为 unsigned char,避免 128~255 的字节被符号扩展。

场景 优化前(ns/次) 优化后(ns/次) 耗时减少 更快轮数
addcslashes("\0", "\0") 108.33 67.13 +39.05% 9/9
addcslashes(str_repeat("\0", 32), "\0") 1270.14 128.24 +89.93% 9/9
addcslashes(str_repeat("\0", 4096), "\0") 148128.66 8427.66 +94.33% 9/9

单个 NUL 的耗时减少 39.05%;32 B / 4 KiB 全 NUL 场景分别减少 89.93% / 94.33%。

对应提交:2101993;PR #23680。

重复一次直接复用输入字符串优化 str_repeat

最能看懂的一集。我们都知道 str_repeat 函数的功能是复制字符串。假设我们只需要复制 1 次。那么是不是就可以什么都不做直接返回?是的。

直接在原实现的开头加入

1
2
3
if (mult == 1) {
RETURN_STR_COPY(input_str);
}
场景 优化前(ns/次) 优化后(ns/次) 耗时减少 更快轮数
str_repeat(str_repeat("x", 32), 1) 120.16 87.60 +28.42% 9/9
str_repeat(str_repeat("x", 4 * 1024), 1) 285.32 87.15 +71.13% 9/9
str_repeat(str_repeat("x", 64 * 1024), 1) 5198.10 87.45 +98.31% 9/9
str_repeat(str_repeat("x", 1024 * 1024), 1) 106521.36 86.30 +99.92% 9/9

虽然此处因为 refcount 变化(由于 COW 语义,我们只能返回该字符串的拷贝)导致 JIT 无法准确预测该函数返回值的 refcount,但是相较于这个快速路径而言可以忽略。除此之外未观测到回退场景。

对应提交:93fd714;PR #23681。

通过复用空串与单字节字符串优化 explode

1
explode(string $separator, string $string, int $limit = PHP_INT_MAX): array

当 limit 为负数时,返回分割完成后,剔除最后 abs(limit) 个元素剩下的数组。

1
2
3
4
5
6
<?php
$text = "line1\nline2\nline3\n";
$lines = explode("\n", $text, 0);
// $lines === ["line1","line2","line3", ""]
$lines = explode("\n", $text, -1);
// $lines === ["line1","line2","line3"]

负数 limit 表示去掉最后若干个分段;这一分支先记录分隔位置,再创建需要返回的字符串。当需要返回的字符串只有一个长度的时候,可以应用一个 Zend 内部的优化。

我们看到源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
PHPAPI void php_explode_negative_limit(const zend_string *delim, zend_string *str, zval *return_value, zend_long limit)
{
#define EXPLODE_ALLOC_STEP 64
const char *p1 = ZSTR_VAL(str);
const char *endp = ZSTR_VAL(str) + ZSTR_LEN(str);
const char *p2 = php_memnstr(ZSTR_VAL(str), ZSTR_VAL(delim), ZSTR_LEN(delim), endp);
zval tmp;

if (p2 == NULL) {
/*
do nothing since limit <= -1, thus if only one chunk - 1 + (limit) <= 0
by doing nothing we return empty array
*/
} else {
size_t allocated = EXPLODE_ALLOC_STEP, found = 0;
zend_long i, to_return;
const char **positions = emalloc(allocated * sizeof(char *));

positions[found++] = p1;
do {
if (found >= allocated) {
allocated = found + EXPLODE_ALLOC_STEP;/* make sure we have enough memory */
positions = erealloc(ZEND_VOIDP(positions), allocated*sizeof(char *));
}
positions[found++] = p1 = p2 + ZSTR_LEN(delim);
p2 = php_memnstr(p1, ZSTR_VAL(delim), ZSTR_LEN(delim), endp);
} while (p2 != NULL);

to_return = limit + found;
/* limit is at least -1 therefore no need of bounds checking : i will be always less than found */
for (i = 0; i < to_return; i++) { /* this checks also for to_return > 0 */
ZVAL_STRINGL(&tmp, positions[i], (positions[i+1] - ZSTR_LEN(delim)) - positions[i]);
zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &tmp);
}
efree((void *)positions);
}
#undef EXPLODE_ALLOC_STEP
}

改动将循环中的 ZVAL_STRINGL() 换成 ZVAL_STRINGL_FAST()。

1
2
3
4
5
		for (i = 0; i < to_return; i++) { /* this checks also for to_return > 0 */
- ZVAL_STRINGL(&tmp, positions[i], (positions[i+1] - ZSTR_LEN(delim)) - positions[i]);
+ ZVAL_STRINGL_FAST(&tmp, positions[i], (positions[i+1] - ZSTR_LEN(delim)) - positions[i]);
zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &tmp);
}

我们看到 ZVAL_STRINGL_FAST 宏的逻辑:

1
2
3
4
5
6
7
if (len > 1) {
return zend_string_init(str, len, 0); // 原来的路径
} else if (len == 0) {
return zend_empty_string; // 复用空字符串
} else {
return ZSTR_CHAR((zend_uchar) *str); // 复用单字节字符串
}

长度为 0 时复用全局空字符串,长度为 1 时复用对应字节的 interned 字符串;长度大于 1 时仍分配并复制,就是原来的那个路径。

因此,省去分配、复制和释放的收益针对空片段与单字节片段,并不是任意长度片段都能省掉一次分配。单字节包括 NUL、0x80 等二进制值,并不限于 ASCII。结果数组和位置表仍然需要内存,函数也仍需扫描输入。

当 limit 为负数时,有:

benchmark 调用 优化前(ms/次) 优化后(ms/次) 配对耗时减少
explode(",", str_repeat(",", 100000), -1) 2.626 0.703 72.14%
explode(",", str_repeat("a,", 100000), -1) 1.811 0.743 60.47%

explode 返回值中空串和长度为一的项越多,收益越大。

对应提交:ac53f14;PR #23631。

预分配装文件信息的数组优化 stat, lstat, fstat

这三个函数用于取得文件信息,例如大小、权限和修改时间。stat() 根据路径查询,fstat() 查询已经打开的文件;lstat() 与 stat() 类似,但遇到符号链接时查询链接本身。

1
2
$info = stat('example.txt'); // 文件存在且查询成功时
echo $info['size']; // 文件大小,也可以通过 $info[7] 读取

为了兼容两种取值方式,返回数组同时有数字标签和文字标签,总共 26 个条目。这里的“标签”就是 PHP 数组的键,例如上面的 7 和 'size'。

看原实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
PHPAPI void php_fstat(php_stream *stream, zval *return_value)
{
php_stream_statbuf stat_ssb;
zval stat_dev, stat_ino, stat_mode, stat_nlink, stat_uid, stat_gid, stat_rdev,
stat_size, stat_atime, stat_mtime, stat_ctime, stat_blksize, stat_blocks;
char *stat_sb_names[13] = {
"dev", "ino", "mode", "nlink", "uid", "gid", "rdev",
"size", "atime", "mtime", "ctime", "blksize", "blocks"
};

if (php_stream_stat(stream, &stat_ssb)) {
RETURN_FALSE;
}

array_init(return_value);

ZVAL_LONG(&stat_dev, stat_ssb.sb.st_dev);
ZVAL_LONG(&stat_ino, stat_ssb.sb.st_ino);
ZVAL_LONG(&stat_mode, stat_ssb.sb.st_mode);
ZVAL_LONG(&stat_nlink, stat_ssb.sb.st_nlink);
ZVAL_LONG(&stat_uid, stat_ssb.sb.st_uid);
ZVAL_LONG(&stat_gid, stat_ssb.sb.st_gid);
#ifdef HAVE_STRUCT_STAT_ST_RDEV
ZVAL_LONG(&stat_rdev, stat_ssb.sb.st_rdev);
#else
ZVAL_LONG(&stat_rdev, -1);
#endif
ZVAL_LONG(&stat_size, stat_ssb.sb.st_size);
ZVAL_LONG(&stat_atime, stat_ssb.sb.st_atime);
ZVAL_LONG(&stat_mtime, stat_ssb.sb.st_mtime);
ZVAL_LONG(&stat_ctime, stat_ssb.sb.st_ctime);
#ifdef HAVE_STRUCT_STAT_ST_BLKSIZE
ZVAL_LONG(&stat_blksize, stat_ssb.sb.st_blksize);
#else
ZVAL_LONG(&stat_blksize,-1);
#endif
#ifdef HAVE_STRUCT_STAT_ST_BLOCKS
ZVAL_LONG(&stat_blocks, stat_ssb.sb.st_blocks);
#else
ZVAL_LONG(&stat_blocks,-1);
#endif
/* Store numeric indexes in proper order */
zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &stat_dev);
zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &stat_ino);
zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &stat_mode);
zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &stat_nlink);
zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &stat_uid);
zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &stat_gid);
zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &stat_rdev);
zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &stat_size);
zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &stat_atime);
zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &stat_mtime);
zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &stat_ctime);
zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &stat_blksize);
zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &stat_blocks);
/* 后面省略 */
}

旧实现从小容量数组开始,先放数字键,再放文字键。数组装不下时需要扩大空间;加入文字键时,还需要换成能够按名称查找的存储形式。

可是问题是,我们早早就知道这个数组有 26 个元素了。为啥还要等动态扩容。直接分配一个 26 个 zval 需要的内存不就完了。

1
2
3
-	array_init(return_value);
+ array_init_size(return_value, 2 * (sizeof(stat_sb_names) / sizeof(*stat_sb_names)));
+ zend_hash_real_init_mixed(Z_ARRVAL_P(return_value));

然后我们用相应的 *_new() API 就好了。这里总的 diff:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
PHPAPI void php_fstat(php_stream *stream, zval *return_value)
{
php_stream_statbuf stat_ssb;
zval stat_dev, stat_ino, stat_mode, stat_nlink, stat_uid, stat_gid, stat_rdev,
stat_size, stat_atime, stat_mtime, stat_ctime, stat_blksize, stat_blocks;
- char *stat_sb_names[13] = {
+ char *stat_sb_names[] = {
"dev", "ino", "mode", "nlink", "uid", "gid", "rdev",
"size", "atime", "mtime", "ctime", "blksize", "blocks"
};

if (php_stream_stat(stream, &stat_ssb)) {
RETURN_FALSE;
}

- array_init(return_value);
+ array_init_size(return_value, 2 * (sizeof(stat_sb_names) / sizeof(*stat_sb_names)));
+ zend_hash_real_init_mixed(Z_ARRVAL_P(return_value));

ZVAL_LONG(&stat_dev, stat_ssb.sb.st_dev);
ZVAL_LONG(&stat_ino, stat_ssb.sb.st_ino);
ZVAL_LONG(&stat_mode, stat_ssb.sb.st_mode);
ZVAL_LONG(&stat_nlink, stat_ssb.sb.st_nlink);
ZVAL_LONG(&stat_uid, stat_ssb.sb.st_uid);
ZVAL_LONG(&stat_gid, stat_ssb.sb.st_gid);
#ifdef HAVE_STRUCT_STAT_ST_RDEV
ZVAL_LONG(&stat_rdev, stat_ssb.sb.st_rdev);
#else
ZVAL_LONG(&stat_rdev, -1);
#endif
ZVAL_LONG(&stat_size, stat_ssb.sb.st_size);
ZVAL_LONG(&stat_atime, stat_ssb.sb.st_atime);
ZVAL_LONG(&stat_mtime, stat_ssb.sb.st_mtime);
ZVAL_LONG(&stat_ctime, stat_ssb.sb.st_ctime);
#ifdef HAVE_STRUCT_STAT_ST_BLKSIZE
ZVAL_LONG(&stat_blksize, stat_ssb.sb.st_blksize);
#else
ZVAL_LONG(&stat_blksize,-1);
#endif
#ifdef HAVE_STRUCT_STAT_ST_BLOCKS
ZVAL_LONG(&stat_blocks, stat_ssb.sb.st_blocks);
#else
ZVAL_LONG(&stat_blocks,-1);
#endif
/* Store numeric indexes in proper order */
- zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &stat_dev);
- zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &stat_ino);
- zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &stat_mode);
- zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &stat_nlink);
- zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &stat_uid);
- zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &stat_gid);
- zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &stat_rdev);
- zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &stat_size);
- zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &stat_atime);
- zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &stat_mtime);
- zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &stat_ctime);
- zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &stat_blksize);
- zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &stat_blocks);
+ zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &stat_dev);
+ zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &stat_ino);
+ zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &stat_mode);
+ zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &stat_nlink);
+ zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &stat_uid);
+ zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &stat_gid);
+ zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &stat_rdev);
+ zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &stat_size);
+ zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &stat_atime);
+ zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &stat_mtime);
+ zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &stat_ctime);
+ zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &stat_blksize);
+ zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &stat_blocks);
/* 后面省略 */
}

这样,我们大概能对以上三个函数观测到 10%~15% 左右的性能提升。

优化 & 重构 pathinfo

这个花了我三天时间。绝对可以单独写一篇 blog 了。但是因为我已经要写力竭了我直接上结果吧。

优化后的 bench:

pathinfo('/var/www/html/index.php', PATHINFO_BASENAME):较原来的运行时间低 46%
pathinfo('/var/www/html/index.php', PATHINFO_EXTENSION):较原来的运行时间低 35%
pathinfo('/var/www/html/index.php', PATHINFO_FILENAME):较原来的运行时间低 40%
pathinfo('/var/www/html/README', PATHINFO_FILENAME):较原来的运行时间低 52%(这是因为 README 没有扩展名,在我的重写里还会省下一次内存分配)

主要是我整个把这个函数重写了。优化了非常多地方,讲肯定是讲不完的 :)

其他

还有一些乱七八糟的小优化。这些优化都测不出来明显的性能差距了,就看个乐吧:

  • ext/standard error_get_last() 使用已知的新键构造错误信息数组,减少键更新时不必要的哈希检查。(commit 6541e83)
  • ext/standard getopt() 使用已知的新键插入选项结果,减少解析命令行选项时的哈希检查开销。(commit f0ed2c5)
  • ext/standard fgetcsv() 和 str_getcsv() 在解析 CSV 字段时使用追加新元素的插入路径,减少数组插入开销。(commit a318ae8)
  • ext/standard array_change_key_case() 复制整数键时使用已知的新键插入路径,提升数组键转换性能。(commit 1849f03)

结尾

写力竭了。既然人的一生必然是痛苦的,不如活在当下,去享受身边的美好。

让我们去海边走一圈!