プログラミングとソフトウェア開発

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

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

2026-07-31
2 分で読めます
10 閲覧数
فريق تحرير certi.news
GitHub 如何让大小写折叠达到内存速度

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

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 吉字节。

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

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

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

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

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

减少内存分配

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 个范围。它还使用并行比较,一次处理八个键,以确定适用的范围。

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

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

结果与测量限制

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

GitHub 确认,这些数字和比较比例仅供参考,不能原样适用于不同处理器,因为它们取决于自动向量化、SWAR、little-endian 字节计算,以及内存带宽和处理器架构。文章将这一思路概括为两个原则:在常见路径上无分支地完成全量扫描;在 Unicode 的罕见路径上直接使用字节空间,而不是解码字符后再重新编码。

ニュースの出典
GitHub Blog
原文を開く ↗
ف
著者

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

同じカテゴリー

おすすめ記事

すべてのニュースを見る