一、函数功能概述
sds sdscatrepr(sds s, const char *p, size_t len)
函数的核心功能是将字符串p
追加到字符串s
中。在追加过程中,它会对字符串p
中的字符进行判断,使用isprint()
函数识别不可打印字符,并对这些字符进行转义处理,确保最终追加后的字符串s
符合特定的格式要求 。
此函数主要用在Monitor模式下。
二、原始版本代码分析
sds sdscatrepr(sds s, const char *p, size_t len) {s = sdscatlen(s,"\"",1);while(len--) {switch(*p) {case '\\':case '"':s = sdscatprintf(s,"\\%c",*p);break;case '\n': s = sdscatlen(s,"\\n",2); break;case '\r': s = sdscatlen(s,"\\r",2); break;case '\t': s = sdscatlen(s,"\\t",2); break;case '\a': s = sdscatlen(s,"\\a",2); break;case '\b': s = sdscatlen(s,"\\b",2); break;default:if (isprint(*p))s = sdscatprintf(s,"%c",*p);elses = sdscatprintf(s,"\\x%02x",(unsigned char)*p);break;}p++;}return sdscatlen(s,"\"",1);
}
2.1 实现逻辑
- 首先在字符串
s
末尾追加双引号"
。 - 然后通过
while
循环遍历字符串p
,对每个字符进行判断:
- 若字符是
\
或"
,使用sdscatprintf
函数将其转义后追加到s
中。 - 若字符是换行符
\n
、回车符\r
、制表符\t
、响铃符\a
、退格符\b
,分别使用sdscatlen
函数将对应的转义序列追加到s
中。 - 对于其他字符,先判断是否为可打印字符。如果是,使用
sdscatprintf
函数以格式化字符形式追加;如果不是,将其以十六进制字符串形式追加到s
中。
- 遍历结束后,在字符串
s
末尾再追加一个双引号"
。
2.2 性能瓶颈
-
频繁内存申请:目标字符串
s
的可用空间不一定能容纳字符串p
,在执行sdscatlen
和sdscatprintf
函数时,若空间不足会触发s
的重新空间申请。当字符串p
较长时,频繁的空间申请操作会严重影响函数性能。 -
可打印字符处理低效:对于可打印字符,使用
sdscatprintf(s,"%c",*p);
进行格式化追加,相比直接赋值操作,这种方式会调用vsnprintf
函数,额外的函数调用和格式化处理消耗了大量性能。
三、预分配版本代码分析
sds sdscatrepr(sds s, const char *p, size_t len) {s = sdsMakeRoomFor(s, len + 2);s = sdscatlen(s,"\"",1);while(len--) {switch(*p) {case '\\':case '"':s = sdscatprintf(s,"\\%c",*p);break;case '\n': s = sdscatlen(s,"\\n",2); break;case '\r': s = sdscatlen(s,"\\r",2); break;case '\t': s = sdscatlen(s,"\\t",2); break;case '\a': s = sdscatlen(s,"\\a",2); break;case '\b': s = sdscatlen(s,"\\b",2); break;default:if (isprint(*p))s = sdscatlen(s, p, 1);elses = sdscatprintf(s,"\\x%02x",(unsigned char)*p);break;}p++;}return sdscatlen(s,"\"",1);
}
3.1 优化思路
-
空间预分配:在函数入口处,通过
s = sdsMakeRoomFor(s, len + 2);
提前为字符串s
分配足够的空间,其中+2
是为了容纳首尾的双引号。这样在大多数情况下,后续追加字符串p
时,不会频繁触发s
的内存重新申请,减少了内存操作的开销。 -
可打印字符处理优化:将处理可打印字符的
s = sdscatprintf(s,"%c",*p);
替换为s = sdscatlen(s, p, 1);
,避免了调用vsnprintf
函数,直接使用memcpy
进行字符复制,提高了可打印字符追加的效率。
3.2 优化效果
测试场景 | 吞吐量(requests per second) | 平均延迟(msec) | 最小延迟(msec) | 50% 分位延迟(msec) | 95% 分位延迟(msec) | 99% 分位延迟(msec) | 最大延迟(msec) |
---|---|---|---|---|---|---|---|
优化前(无监控) | 264669.28 | 1.663 | 0.304 | 1.623 | 2.487 | 3.383 | 45.855 |
优化前(1 个监控) | 78633.66 | 5.917 | 0.312 | 4.815 | 11.711 | 31.775 | 171.391 |
优化后(无监控) | 255761 | 1.710 | 0.320 | 1.631 | 2.727 | 3.579 | 36.415 |
优化后(1 个监控) | 106142.47 | 4.347 | 0.328 | 3.695 | 7.783 | 18.031 | 107.711 |
从结果看吞吐从788633.66/s提升到了106142.47,提升幅度约34%。
3.3 仍存在的问题
-
尽管进行了空间预分配,但当字符串
p
中存在大量不可见字符时,由于对不可见字符的处理方式仍可能导致空间不足,进而触发s
的内存重新申请,影响性能。 -
sdscatlen
函数分析
sds sdscatlen(sds s, const void *t, size_t len) {size_t curlen = sdslen(s);s = sdsMakeRoomFor(s, len);if (s == NULL) return NULL;memcpy(s + curlen, t, len);sdssetlen(s, curlen + len);s[curlen + len] = '\0';return s;
}
从该函数逻辑可以看出,对于可打印字符,在使用sdscatlen
函数追加时,每追加一个字符就会调用一次memcpy
函数。当处理长字符串p
时,频繁调用memcpy
会带来一定的性能损耗。
四、批处理版本代码分析
sds sdscatrepr(sds s, const char *p, size_t len) {s = sdsMakeRoomFor(s, len + 2);s = sdscatlen(s, "\"", 1);while (len) {if (isprint(*p)) {const char *start = p;while (len && isprint(*p)) {len--;p++;}s = sdscatlen(s, start, p - start);} else {switch (*p) {case '\\':case '"': s = sdscatprintf(s, "\\%c", *p); break;case '\n': s = sdscatlen(s, "\\n", 2); break;case '\r': s = sdscatlen(s, "\\r", 2); break;case '\t': s = sdscatlen(s, "\\t", 2); break;case '\a': s = sdscatlen(s, "\\a", 2); break;case '\b': s = sdscatlen(s, "\\b", 2); break;default:s = sdscatprintf(s, "\\x%02x", (unsigned char)*p);break;}p++;len--;}}return sdscatlen(s, "\"", 1);
}
4.1 优化逻辑
-
调整判断顺序:在遍历字符串
p
时,优先判断字符是否为可打印字符。如果是,以当前字符为起始点,继续判断后续字符是否也为可打印字符,直到遇到非可打印字符或字符串结束。 -
批量追加:通过记录可打印字符的起始位置和长度,使用
sdscatlen(s, start, p - start);
一次性将连续的可打印字符追加到字符串s
中。这种方式减少了memcpy
函数的调用次数,相比原始版本和预分配版本,进一步提高了可打印字符追加的效率,尤其在处理包含大量连续可打印字符的字符串时,性能提升更为显著。
4.2 优化效果
测试场景 | 吞吐量(requests per second) | 平均延迟(msec) | 最小延迟(msec) | 50% 分位延迟(msec) | 95% 分位延迟(msec) | 99% 分位延迟(msec) | 最大延迟(msec) |
---|---|---|---|---|---|---|---|
优化前(无监控) | 714081.69 | 0.654 | 0.112 | 0.687 | 0.847 | 0.879 | 3.247 |
优化前(1 个监控) | 332967.06 | 1.449 | 0.384 | 1.647 | 1.855 | 1.927 | 4.263 |
优化后(无监控) | 714030.69 | 0.645 | 0.128 | 0.671 | 0.839 | 0.871 | 2.839 |
优化后(1 个监控) | 395116.38 | 1.221 | 0.248 | 1.367 | 1.575 | 1.631 | 3.735 |
通过对比可以发现,在有1个监控的场景下,优化后的版本吞吐量从332967.06/s提升至395116.38/s,提升幅度约为18%,且各分位延迟也有所改善,充分证明了批量处理可打印字符的优化策略对sdscatrepr函数性能提升的有效性。