如果你写过几年代码,大概都经历过这样一个瞬间:在几十万行的代码库里 grep 一个变量名,终端像卡住了一样转了十几秒才吐出结果。然后有人告诉你:"试试 rg(ripgrep)。"你半信半疑地敲下去,结果几乎是回车即出。同一个搜索,ripgrep 经常比 GNU grep 快上数倍,比 The Silver Searcher、ack 这类"现代替代品"快得更多。
这个视频做的,就是把这种"快得离谱"的黑盒撬开给你看。但老实说,单靠视频那十几分钟的演示,你只会记住"ripgrep 很快"这个结论,却抓不住它为什么快。真正值钱的洞察,藏在作者 Andrew Gallant(网名 BurntSushi)那篇 2016 年的经典长文里——ripgrep 的速度不是任何一招鲜,而是把字符串搜索、自动机理论、CPU 内存层级、并行调度四件事拧成一股绳的工程组合拳。
这篇解读的任务,就是把那股绳拆开,再帮你重新编好。看完你会明白:ripgrep 真正颠覆的,是"搜索"这件事在计算机里到底该怎么算——它本质上不是在"读文本",而是在"让字节自己撞进一张预先布好的网"。掌握这个思维,你以后看任何"为什么 X 这么快"的问题,都会多一个趁手的分析框架。
| 项目 | 内容 |
|---|---|
| 视频标题 | I Thought GREP Was Enough… Then Ripgrep Made Me Faster |
| 核心主题 | ripgrep 相对 grep 的速度优势与日常实战用法 |
| 作者/背景 | 基于 BurntSushi(Andrew Gallant)用 Rust 开发的 ripgrep |
| 首发标杆文 | 《ripgrep is faster than {grep, ag, git grep, ucg, pt, sift}》(2016) |
| 关键技术栈 | Rust regex crate · 惰性 DFA · Teddy SIMD · mmap · 多线程 |
| 核心论点 | 速度来自"字面量预过滤 + 自动机引擎 + 智能剪枝"的组合,而非单一技巧 |
要理解 ripgrep 为什么快,得先看它的对手——传统正则引擎——慢在哪。视频里一笔带过的"正则可能很慢",背后的真相比想象中严重得多。
主流的 PCRE、Python re、Java、Ruby 的正则引擎,普遍采用回溯(backtracking)算法来支持回溯引用、前瞻后顾这些"高级特性"。回溯引擎在遇到歧义模式时,会一条路一条路地试,走不通就退回来换一条。问题在于,这种试探在某些输入上会指数级爆炸——也就是著名的 ReDoS(正则拒绝服务)漏洞。一段几十个字符的恶意字符串,就能让一个回溯引擎卡死几小时甚至几年。
ripgrep 的底层引擎(Rust 的 regex crate)做了一个近乎"反潮流"的决定:放弃回溯,只用有限自动机(finite automata)。它只支持正则的"正交子集"——那些能被 NFA/DFA 精确描述的模式,拒绝任何会导致回溯的特性。代价是你写不了某些花哨的正则;回报是,它对任何输入都能保证线性时间,永远不会出现"卡死"。这是 ripgrep 一切速度的地基:一个不会跑歪的引擎,才值得在上面叠加优化。
这是 BurntSushi 博客里点出的关键设计哲学,视频没有展开——但对理解整个工具至关重要。
很多人以为 ripgrep 拿到 foo.*bar 这样的正则后,就老老实实对每个字符跑状态机。错了。它的第一步是把正则里的字面子串抽出来。
引擎会分析模式,发现里面有一个固定字面量 foo。于是它根本不急着跑那个 .* 的正则,而是先用一个极快的字面量搜索去文本里找 foo 出现的位置。只有当 foo 这个"嫌疑位置"被找到后,它才在附近启动完整的正则状态机去验证 .*bar 是否真的成立。如果整段文本里压根没有 foo,正则引擎几乎一行都不用真正"跑"。
这就是 ripgrep 最关键的一个思想:绝大多数搜索请求,瓶颈不在正则本身,而在于你用多快的速度跳过那些根本不可能匹配的区域。博客里反复出现 128 次的 "literal" 字样,正说明作者把多少心血压在了这一层。这个"字面量预过滤(literal prefilter)"的思路,也是后来无数高性能搜索工具的共同套路。
光有"先找字面量"还不够,因为"找字面量"本身也是一个搜索问题。如果还是逐字节比对,速度依然上不去。ripgrep 在这一层祭出了真正的硬件杀手锏——SIMD(单指令多数据)。
现代 CPU 都有 128 位(SSE)或 256 位(AVX2)的向量寄存器,一条指令能同时处理 16 或 32 个字节。ripgrep 用的是一套叫 Teddy 的多模式 SIMD 搜索算法。Teddy 不是 BurntSushi 发明的,它出自 Intel 的 Hyperscan 正则库,由工程师 Geoffrey Langdale 设计。BurntSushi 把它移植到了 Rust 的 regex crate 里。
Teddy 的精妙之处在于:它能同时搜索多个字面量,并且利用 SIMD 把每个 16/32 字节的窗口一次性筛一遍,用位运算快速判断"这个窗口里有没有可能出现我要找的字节组合"。只有窗口亮了红灯,才回退到精确验证。这意味着 ripgrep 不是"一个字符一个字符地读文本",而是"一把一把地吞字节",吞吐量直接乘以 16。
视频把这个细节简化成了"ripgrep 用了 SIMD",但 Teddy 多模式并行扫描这个内核,才是 SIMD 真正发挥威力的地方——尤其当正则里能抽出多个字面量时(比如
(cat|dog|bird)),收益会被进一步放大。
字面量搜到了候选位置,接下来才是真正"跑正则"。这里 ripgrep 用的是确定性有限自动机(DFA)。
回溯引擎是"试探+退回",而 DFA 是一条确定性的路径:当前处于哪个状态、读入哪个字节,下一个状态完全确定,没有任何分叉、没有回退。每个字节处理时间恒定,整体就是严格的 O(n) 线性时间。更妙的是,ripgrep 的引擎把 UTF-8 解码直接编译进了状态机——也就是说,它不需要先把字节流解码成 Unicode 字符再匹配,状态机自己就知道怎么在原始字节流上一边"读懂 UTF-8"一边匹配 \p{Greek} 这样的 Unicode 类。
但 DFA 有个老问题:某些正则会产生状态爆炸,把 DFA 变成天文数字的状态表。ripgrep(继承 Rust regex 的设计)用的是惰性 DFA(lazy DFA):它不预先构建整张状态表,而是边跑边建——只有真正被访问到的状态才会被生成并缓存。这样既保留了 DFA 的线性速度,又规避了状态爆炸。博客里 "dfa" 出现 25 次,足见这一层的分量。
读完算法,还有一道"搬运"的坎:文件在硬盘上,程序怎么把它读进来?最朴素的方式是 read() 系统调用,把字节一段段拷进用户态缓冲区。ripgrep 的另一招是 mmap(内存映射)——把文件直接映射到进程的虚拟地址空间,让操作系统的页缓存(page cache)在背后按需把磁盘页调入内存。
好处是:避免了用户态和内核态之间的字节拷贝;多个进程读同一个文件时能共享同一份页缓存;访问模式交给 OS 调度,往往更聪明。对于单个大文件的搜索,mmap 通常更快。
但这里有个反直觉的细节,恰恰体现了 BurntSushi 作为工程师的清醒:他明确指出,"内存映射总是更快"是个误区。当你要搜索成千上万个小文件时,mmap 反而可能更慢——因为每个文件都要建立映射、产生缺页中断,开销被摊薄不掉。所以 ripgrep 并不无脑用 mmap,而是用启发式策略:大文件用 mmap,小文件用普通读缓冲。这种"知道何时不用某项优化"的判断力,比优化本身更难得。
前面几招都是"搜得更快",这一招是"根本不搜"。博客里点明:ripgrep 不仅引擎快,它还"tries to be smart about what it searches"。
具体来说,ripgrep 默认会尊重 .gitignore——那些被版本控制忽略的 node_modules、target、构建产物,它直接跳过,连打开都不打开。它还会自动跳过二进制文件和隐藏文件。对于真实代码库,这一步经常比任何算法优化都更立竿见影:你以为在搜 50 万行代码,剪枝后真正要扫的可能只有 5 万行。
剪枝之后,剩下的文件被分发给多个线程并行搜索。ripgrep 用工作窃取式的调度,把文件分摊到 CPU 各个核心上同时跑。这里有个工程难点:多线程共享正则引擎的 DFA 缓存会引发竞争,所以 ripgrep 在线程同步上做了精心设计(mutex 保护的只读状态 + 每线程本地状态),既榨干并行度又不被锁拖慢。
公平地说,GNU grep 本身并不慢。它在底层也用了 Aho-Corasick、Boyer-Moore 这些经典的高效字面量算法,单文件搜索速度极强。视频里"快十倍"更多是相对 The Silver Searcher、ack 这些工具,以及"在真实代码库里的端到端体验"。
那 ripgrep 相对 GNU grep 的优势到底在哪?主要有三点:第一,默认尊重 .gitignore——grep 默认不尊重,会把 node_modules 一类垃圾全扫一遍;第二,多线程并行——grep 是单线程;第三,Rust regex 的"字面量+惰性 DFA"组合在复杂 Unicode 正则上比 grep 的回退引擎更稳,且不会 ReDoS。再加上 Rust 带来的内存安全和跨平台一致体验,ripgrep 才在开发者工具的生态里站稳了"现代默认"的位置。
这一层对比是我基于博客的延伸分析,帮你厘清"快"到底快在引擎、还是快在工程取舍。
flowchart TD
A[用户输入正则模式<br/>如 foo.*bar] --> B{引擎分析阶段}
B --> C[抽取出字面量 foo]
B --> D[惰性构建 DFA 状态机]
C --> E[Teddy SIMD 多模式扫描<br/>一次吃 16/32 字节]
E --> F{窗口命中字面量?}
F -->|否| G[整块跳过,不跑正则]
F -->|是| H[在候选位置启动 DFA 验证]
D --> H
H --> I[O(n) 线性输出匹配]
J[文件读取层] --> K{文件大小判断}
K -->|大文件| L[mmap 内存映射<br/>内核页缓存搬运]
K -->|海量小文件| M[普通 read 缓冲]
L --> E
M --> E
N[调度与剪枝层] --> O[尊重 .gitignore<br/>跳过二进制/隐藏文件]
N --> P[多线程并行分发文件]
P --> E
style E fill:#ffe4b5
style H fill:#b5e4ff
style G fill:#d4f4dd
对日常开发者: 这套拆解最大的迁移价值,不是让你去重写一个 ripgrep,而是给你一个判断"性能问题出在哪一层"的思维模型。下次你的搜索/过滤逻辑慢了,别急着换语言、换库,先按 ripgrep 的五层过一遍:
这个清单比任何具体技巧都值钱,因为它适用于字符串搜索之外的几乎所有性能战场——从日志扫描、数据清洗到推荐召回。
对工程审美: ripgrep 还示范了一种被低估的品质——"知道何时不优化"。它放弃回溯换来可预测性,在小文件上放弃 mmap 换来吞吐,在正则特性上做减法换来线性保证。真正的性能高手,往往不是把所有优化全开,而是对每一层都清楚它的代价边界。这一点,比"它用了 SIMD"更值得记一辈子。
一个可立刻上手的小练习: 在你最大的代码库里分别跑 time grep -rn "PATTERN" . 和 time rg "PATTERN",再各加一次忽略 .gitignore 的对照。亲眼看到数量级的差异,这套理论就会从"别人的故事"变成"你的肌肉记忆"。
regex crate 文档与博客 — 作者关于"惰性 DFA""字面量提取"的系列文章,自动机工程化的最佳教材。ripgrep 的快,不是因为它读了更多文本,而是因为它用字面量预过滤 + SIMD 批扫描,让绝大多数字节连正则都不用跑——性能的终极秘密,永远是"少干活"。
本文由「51学通信」技术团队出品。51学通信由通信行业18年老兵爱卫生(微信ID: gprshome201101)维护,聚焦5G/6G核心网、3GPP标准解读、通信前沿技术深度分析。 出版作品有《5G核心网原理与实践》《IMS原理与实践》等4本通信专业书籍。知识星球「51学通信」提供深度技术答疑和行业洞察。
关键洞察提炼: ripgrep 的速度本质是"少干活"——字面量预过滤 + SIMD 批扫描让绝大多数字节不触发正则,惰性 DFA 保证线性时间,mmap/剪枝/并行各司其职,且工程师"知道何时不用某优化"是更难得的品质。
注意事项:
- 锚定视频为真实存在的 ripgrep 讲解视频(长视频资源较稀缺,该主题深度内容主要沉淀在 BurntSushi 博客,已在文中透明标注视频观点与博客延伸的边界)
- 临时缓存文件 /home/gprshome/rg_blog.txt 已生成,如不需要可删除
[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。
