最近我正在读 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); 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)); 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 : *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 = explode ("\n" , $text , -1 );
负数 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 ) { } 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; 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; for (i = 0 ; i < to_return; i++) { 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' ];
为了兼容两种取值方式,返回数组同时有数字标签和文字标签,总共 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 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 )
结尾 写力竭了。既然人的一生必然是痛苦的,不如活在当下,去享受身边的美好。
让我们去海边走一圈!