
做文本处理的团队经常会遇到类似的情况:一段代码要在内存里快速扫过,把里面的字母统一折叠成小写,供后面的大小写不敏感匹配使用。数据量一大,第一反应就是把检测和转换合并成一趟,心想少读一遍内存总是快的。GitHub 工程师最近在博客里记录了一次完整的对比实验,结论和直觉正好相反:两趟分开做,吞吐量是融合方案的 2.6 倍。
融合一趟为什么输给了拆成两趟
这个问题的场景是处理源代码文本,在 64 位系统上每 16 字节检测一次,用两个 u64 通道做或运算,再拿一个 `& 0x8080_8080_8080_8080` 掩码检查高位。检测阶段不写内存,转换阶段用向量化指令一次处理 16 字节。
先试的是把两者融合:检测到非 ASCII 字符就当场转换,数据只读一遍。结果很尴尬,吞吐量只有 8.7 GiB/s。拆成两阶段后,先无分支地扫描确认 ASCII 前缀,再单独做向量化转换,吞吐量到了 23 GiB/s------将近三倍差距。
问题出在分支上。融合方案每 16 字节就要做一次分支判断,编译器看到分支就没法对整个循环做向量化,内层转换即使还能向量化,也扛不住外层分支带来的延迟。GitHub 工程师的判断是:在热点循环里,分支开销对性能的杀伤力远大于多读一遍数据。数据访问次数减半,但分支导致编译器放弃优化,整体反而更慢。

预分配缓冲区里的第二个取舍
两阶段方案还有个细节:为了避免内存增长触发的重分配,用预分配缓冲区,只在必要时才扩展。这换来了另一个权衡------检测阶段和转换阶段都要读一遍数据,内存带宽压力是融合方案的两倍。
这里其实是两个取舍叠在一起:一个是"少读数据"对"无分支可向量化",另一个是"避免重分配"对"带宽翻倍"。GitHub 的结论是前者后者都选了后者,实测数据支持这个选择。
但不要把这个结论推广成"两趟永远比一趟快"。这次能赢,前提是检测阶段做到了完全无分支,并且转换阶段能被编译器向量化。如果检测逻辑本身复杂到无法消除分支,或者数据源不在内存里而在磁盘上,两趟读的成本结构就完全不一样了。融合方案在有些场景仍然合理,这次实验说明的是:少读一遍数据不值得用分支换。
对做底层优化的工程师来说
这类经验在编译器后端、文本编辑器的语法高亮、数据清洗工具里都很常见。很多团队优化热点路径时,第一个想到的就是"减少内存访问次数",毕竟带宽在纸面上是最贵的资源。但编译器优化和 CPU 分支预测的实际行为,往往比内存带宽更能决定吞吐量。
实测里有个容易被忽略的细节:两阶段方案里内层 block convert 仍然向量化成了单个 16 字节操作。也就是说,把"检测"和"转换"分开,反而让每一半都能独立优化到极致------检测无分支无存储,转换向量化无分支。各自做到最好,合起来比勉强融合更强。
目前没有公开信息说明这个算法在不同架构(比如 32 位或 ARM)上的表现,也没有部署成本和维护复杂度的数据。GitHub 分享的主要是一个优化思路:当你在热点循环里纠结要不要省一次内存访问时,先想想分支会不会让编译器放弃向量化。这个取舍的顺序,比"少读一遍"本身更值得记下来。
关于维基框架
维基框架关注企业应用开发中的长期维护问题。在实际项目中,业务系统往往同时涉及权限、微服务、接口协议、部署环境等复杂因素,因此我们希望提供一套更容易扩展和维护的基础框架。
Gitee:gitee.com/wiki-framework
GitHub:github.com/wiki-framework
示例项目:gitee.com/cdkjframework/framewiki-example
📄 许可证:MulanPSL-2.0(木兰宽松许可证,第2版)