strlcpy 以及 CPU 如何违背常识

2024 年 7 月 24 日,作者关于strlcpy的旧帖在各论坛引发讨论,可能与最近发布的 POSIX 版本有关。一个常见论点是在源字符串适合目标缓冲区的常见情况下,strlcpy只需遍历字符串一次,而strlen + memcpy总是遍历两次。但实际上,遍历字符串一次不一定更快。

strlcpy起源于 openbsd,其实现是先尽可能从源复制到目标,若因目标空间不足而截断,则遍历剩余源以获取strlen(src)值返回。而 glibc 的strlcpy实现首先获取源字符串长度,再使用memcpy复制,这打破了strlcpy只遍历一次字符串的错觉,实际上 major libcs 中的strlcpy总是遍历字符串两次。

作者编写了一个接近 glibc 实现的bespoke_strlcpy函数,它遍历源字符串两次,与 openbsd 版本相比,在复制 512 字节和 1MiB 字符串时分别快约两倍。这是因为bespoke_strlcpystrlen循环存在循环携带依赖,而memcpy循环没有,openbsd 版本的长度和复制循环融合在一起,导致依赖影响了复制操作,且依赖对 CPU 和编译器优化都有困难,造成更差的代码生成。

此外,作者认为字符串长度是宝贵信息,nul-terminated 字符串易导致反复计算长度,而有零拷贝子字符串可避免大量不必要的复制和内存管理。如今作者使用 sized-strings ,仅在外部 API 要求时转换为 nul-string。大多数编程语言已意识到 nul-strings 的问题,即使有 C heritage 的语言也在远离 nul-strings。

最后总结,讨论性能时要明确是在学术还是实际环境中,CPU 不在乎常识或大 O 表示法,算法性能不仅取决于高级算法因素,还需考虑低级因素如缓存缺失、ILP、分支预测错误等,很多看似从常识上更快的做法实际可能更慢,反之亦然。

阅读 15
0 条评论