编程与软件开发

GitHub 如何让大小写折叠达到内存速度

GitHub 介绍了如何将代码搜索引擎 Blackbird 中的大小写折叠速度提升到单核每秒超过 45 GB:通过从快速路径中移除控制分支,并使用面向字节的计算处理 Unicode。该公司已将这一方法以名为 casefold 的开源 Rust 库形式发布。

2026-07-31
2 分钟阅读
10 浏览量
فريق تحرير certi.news
GitHub 如何让大小写折叠达到内存速度

GitHub 通过重新设计软件循环,使大小写折叠(Case Folding)的速度在单个核心上超过每秒 45 GB。该循环不会在遇到第一个非 ASCII 字节时停止,而是完整扫描缓冲区,不使用依赖数据的控制分支。公司在代码搜索引擎 Blackbird 中使用这一过程;该引擎为超过 1.8 亿个代码仓库和超过 480 TB 的源代码建立索引。

Alexander Neubeck 和 Greg Orzell 于 2026 年 7 月 31 日在 GitHub 博客发布的文章中介绍了这一设计的细节。文章还宣布将成果以名为casefold的开源 Rust 库形式发布。

大小写折叠不只是转换为小写

搜索引擎和文本匹配工具需要一种标准表示形式,使大小写不同的字符串在比较时相等。这一需求出现在搜索、不区分大小写的正则表达式、用户名和主机名中。

但将文本转换为小写并不能实现同样的目的。小写转换可能取决于语言和上下文,例如希腊字母 sigma 在词尾和词中的形态不同,或者土耳其语中字母 I 的规则不同。而大小写折叠是为比较而设计的,因此与语言和上下文无关。在德语字母 ß、土耳其语字母 İ 以及希腊语词尾 sigma 等情况下,结果也会有所不同。

该库按照 Unicode 字符数据库附带的 CaseFolding.txt 文件中的 C 和 S 两种状态,执行一次一对一的简单折叠。它不执行多字符折叠操作,例如将 ß 转换为 ss,也不执行土耳其语专用的折叠操作。

移除曾经拖慢循环的优化

由于源代码大多由 ASCII 字符组成,最快路径主要将 A 到 Z 的大写拉丁字母转换为小写。直观的设计是在发现非 ASCII 字节后立即停止,然后将剩余文本交给 Unicode 路径处理。但在 Apple M4 处理器上的测试显示,这种方法的速度只有约每秒 3 GB。

主要原因是循环内部的控制分支。算法不再测试每个字节并提前停止,而是将所有字节的最高位汇总到一个变量中,并在扫描结束后再测试结果。至于字节是否处于大写字母范围内,则通过计算实现:对字节减去字母 A,并利用回绕,然后将结果与数字 26 比较。之后使用计算掩码设置字节中的第五位,从而在没有分支或条件写入的情况下将大写字母转换为小写。

这种结构允许 LLVM 编译器使用 Apple M4 上的 NEON 生成向量化指令,每次处理 16 个字节。结果超过每秒 45 GB,接近内存带宽上限。GitHub 的测量表明,移除提前退出是实现向量化的关键因素;保留基于数据的退出机制会阻止生成向量化指令,即使循环的其余部分已经没有分支。

为什么合并扫描并不总是更快?

GitHub 测试了一种折中方案:按块检查 ASCII,然后转换 ASCII 前缀。这种方法会读取数据两次,但速度达到约每秒 23 GB,远快于朴素循环,同时仍能在遇到第一个非 ASCII 块时停止。

相比之下,将检查和转换合并到一个每次处理 16 字节的循环中,速度反而更慢,约为每秒 8.7 GB,而两遍处理方案为每秒 23 GB。根据文章,块处理后执行的提前退出分支会阻止编译器展开循环,或隐藏读取、测试、转换和写入之间的等待时间。因此,两个干净且可向量化的循环胜过一个接触数据次数更少、但包含依赖内容的分支的循环。

减少内存分配

函数 simple_fold 按所有权接收一个 String,因此可以修改其缓冲区并直接返回。如果文本完全由 ASCII 组成,转换字符后会直接返回同一块内存,不需要第二个缓冲区或额外复制。

存在非 ASCII 字符时,算法只有在遇到长度或内容发生变化的字符后才会创建新缓冲区。文章指出,大多数折叠操作会保持或缩短 UTF-8 长度,但 U+023A 和 U+023E 两个字符的长度可能分别从两个字节增加到三个字节。因此,算法只会一次性预留约为输入长度 1.5 倍的最大容量,而不是逐步扩展缓冲区并反复复制数据。

此外,对于未发生变化的字节组,它使用 copy_nonoverlapping 进行搬移,而不是逐字节复制。对于 CJK、Hangul、Kana、阿拉伯语、希伯来语和符号等非拉丁文本,只要不包含需要折叠的字符,算法就会保留其原始分配。

在字节空间中处理 Unicode

Unicode 16.0 包含 1484 个简单折叠操作,但 GitHub 通过利用可折叠字符集中在每页 64 个码点的特性,将其表压缩到 1776 字节。为了测试某个字符是否需要折叠,算法使用位图;如果对应位未启用,就会立即拒绝该字符,无需解码 UTF-8 或查找哈希表。

在包含折叠操作的页面内,算法存储相邻范围,而不是为每个码点保存单独记录。范围由起点、终点、步长和差值描述,将约 1484 个操作缩减为分布在 59 个页面中的 238 个范围。它还使用并行比较,一次对 8 个键进行比较,以确定适用的范围。

找到范围后,折叠字符通过在 UTF-8 层面将两个字节与范围专用常量相加来计算,而不是先将字符解码为码点再重新编码。这样可以处理长度变化,例如将 U+212A——开尔文符号——从三个字节转换为一个字节的字母 k,或将 U+023A 转换为一个三字节字符。

这种方法假设输入是有效且规范的 UTF-8,这是 Rust 中 String 和 str 类型所保证的前提。至于来自其他来源的原始数据,则必须在使用这些计算之前验证其有效性或进行规范化。

结果与测量限制

在常见的 ASCII 情况下,根据文章中的测量结果,该库的速度超过每秒 45 GB,比并不完全等价的 str::to_lowercase 函数快 50% 以上。在需要折叠大多数字符的最坏输入情况下,基于字节空间计算的方案大约是经过优化的 UTF-8 解码和重新编码路径的两倍快。

GitHub 强调,这些数字和比较比例仅供参考,不能直接套用于不同处理器,因为它们取决于自动向量化、SWAR、小端字节序下的字节计算,以及内存带宽和处理器架构。文章将这一思路概括为两个原则:在没有分支的情况下完整扫描常见路径;在字节空间中处理罕见的 Unicode 路径,而不是解码字符后再重新编码。

新闻来源
ف
作者

فريق تحرير certi.news

同一分类

你可能还喜欢

查看所有新闻