读书笔记 · 约 14,024 字 · 预计推送 2 部分

ripgrep 为什么快:正则引擎设计、并行搜索、内存模型(主题精读) 未开始

目录

题记与说明


一、七维提取

1. 核心问题

一个"代码搜索工具"(grep/ack 类)在真实仓库里要做到单大文件与海量小文件两个场景都不慢,难点在哪?ripgrep 作者要回答三个问题:

2. 主要主张

博客开头给出三条可检验声明(原文):

- For both searching single files *and* huge directories of files, no other tool obviously stands above ripgrep in either performance or correctness.
- ripgrep is the only tool with proper Unicode support that doesn't make you pay dearly for it.
- Tools that search many files at once are generally _slower_ if they use memory maps, not faster.

README 把"为什么快"压缩成 5 点(原文):

* It is built on top of Rust's regex engine. Rust's regex engine uses finite automata, SIMD and aggressive literal optimizations to make searching very fast.
* Rust's regex library maintains performance with full Unicode support by building UTF-8 decoding directly into its deterministic finite automaton engine.
* It supports searching with either memory maps or by searching incrementally with an intermediate buffer. The former is better for single files and the latter is better for large directories. ripgrep chooses the best searching strategy for you automatically.
* Applies your ignore patterns in .gitignore files using a RegexSet. That means a single file path can be matched against multiple glob patterns simultaneously.
* It uses a lock-free parallel recursive directory iterator, courtesy of crossbeam and ignore.

3. 方法

(a)正则引擎设计 —— 有限自动机 + 字面量预过滤,而非回溯

This crate tends to be quite good at recognizing literals in a regex pattern and using them to accelerate a search. … in the regex \w+@\w+, the engine will look for occurrences of @ and then try a reverse match for \w+ to find the start position.
perf-literal — Enables the use of literal optimizations for speeding up matches. In some cases, literal optimizations can result in speedups of *several orders of magnitude*.
perf-dfa — Enables the use of a lazy DFA for matching. The lazy DFA is used to compile portions of a regex to a very fast DFA on an as-needed basis. This can result in substantial speedups, *usually by an order of magnitude on large haystacks*.

(b)并行搜索 —— 并行目录遍历 + 锁无关工作窃取

(c)内存模型 —— mmap 不是银弹

Use memory maps when they are believed to be advantageous. The heuristics used to determine whether to use a memory map or not may depend on many things, including but not limited to, *file size and platform*. … By default, memory maps are disabled.
TRACE|grep_searcher::searcher … searching via memory map / slice reader: searching via slice-by-line strategy / searcher core: will use fast line searcher

4. 数据与证据

(a)作者基准(EC2 c3.2xlarge + 作者本地 i7-6900K,Linux kernel built tree / OpenSubtitles 2016,全部可由仓库内 benchsuite 复现):

(b)本会话本地实测(命令与原始输出,3 次取观察,bash time,单位秒):

单文件 300MB、Sherlock Holmes 稀疏 359 命中:

命令3 次 real
rg -c 'Sherlock Holmes' single/big.log(默认,--trace 证实 searching via memory map)0.348 / 0.361 / 0.404
rg --no-mmap -c …0.239 / 0.260 / 0.243
GNU grep 3.0 -c0.123 / 0.285 / 0.141

目录树 48MB/3000 文件(.rgignore 排除一半后默认只搜 23,742,000 字节,--stats 证实;-uuu 搜 47,484,015 字节):

命令3 次 real
rg ERR_SYS tree(默认:并行+忽略)0.520 / 0.230 / 0.242
rg -j1 ERR_SYS tree(串行)0.488 / 0.494 / 0.543
rg -uuu ERR_SYS tree(并行搜全量 2 倍字节)0.351 / 0.331 / 0.277
rg --sort path ERR_SYS tree(排序→禁并行)0.698 / 0.498 / 0.832
GNU grep -r ERR_SYS tree(串行搜全量)1.539 / 1.479 / 1.510

300MB 单文件正则(注意:命中数高达 180 万,属 README 自述的高命中病态区):

--stats 原始片段(tree 默认模式):12000 matches / 3000 files searched / 47484000 bytes searched / 0.151000 seconds spent searching / 0.415357 seconds total(搜索纯耗时只占总时长约 1/3——遍历与打印是另一大块)。

对本地实测的诚实解读:目录树场景与作者结论一致——并行+忽略规则使 rg 默认模式比 grep -r 快约 6 倍(0.23s vs 1.5s),并行化(-j1→默认)在本机约 2 倍;--sort path 确实禁用并行变慢。但单大文件场景 GNU grep 3.0 在这台 Windows 机器上全面更快(连无字面量正则都快 5 倍以上),rg 的 mmap 默认策略在 Windows 单文件上也是负优化(--no-mmap 反超)。这与博客"平台依赖、VM 会惩罚 mmap"的警告方向吻合,也提示:Linux 上 rg 的单文件优势(作者数据 rg 全胜)不能想当然平移到 Windows。

5. 结论

  1. ripgrep 的快是分层的工程结论,不是单点魔法:①有限自动机引擎的 O(m·n) 底线(消灭 ReDoS)②字面量预过滤(多数真实查询根本不进核心引擎)③UTF-8 编进 DFA(Unicode 免费送)④无锁并行目录遍历(多核白拿)⑤ mmap/增量读按场景启发式选择("mmap 更快"是有条件的神话)。
  2. 工具层与引擎层的分工明确:inner-literal 这类"知道自己在搜行"的优化放在 rg,不放在通用引擎里;引擎层只负责保证任意消费者都拿到 lazy DFA/Teddy/Aho-Corasick/memchr 的自动加速(README 原文:大多数优化在 regex 库里,"every consumer of that library gets all these performance optimizations automatically")。
  3. PCRE2 慢的原因可完全归因到两条:失去"静态禁止跨行匹配"导致逐行慢搜 + Unicode 强制 UTF-8 转码;-U --no-pcre2-unicode 可让 PCRE2 JIT 反超(FAQ 实测 2.458s → 1.121s)。这条是把"引擎能力"和"数据供给方式"分开分析的绝佳案例。
  4. 忽略规则(.gitignore/.rgignore)首先是相关性优化,性能上"是严格的加分项但不是必然"——规则编译与逐路径匹配有开销,Linux kernel 树 178 个 ignore 文件的代价被 RegexSet 均摊。

6. 局限

作者自认的:

7. 对我的启发


二、配套终端工具链实践建议(从上述机制直接推导)

配置固化(RIPGREP_CONFIG_PATH 指向一个 rc 文件,每行一个 flag):

# ~/.config/ripgrep/rc
--smart-case          # 小写不敏感、含大写才敏感:不牺牲 Teddy 字面量路径
--stats               # 结束时打印 searched bytes/spent searching,顺便当性能仪表
--colors=line:style:bold

注意 FAQ 原文:结果顺序不定是并行的固有副作用,"The only way to make the order of results consistent is to ask ripgrep to sort the output. Currently, this will disable all parallelism."——脚本里要确定性顺序就 --sort path,交互场景别加。

性能调优速查(每条都有上文机制依据):

  1. 正则能带字面量就带:\w+@\w+ 靠 @ 加速;纯交替 foo|bar 走 Teddy 完全绕过核心引擎;--debug 可看提取结果(本会话实测:extracted fast line regex: "(?:(?:fox)|(?:lazy))")。
  2. Windows/VM 上单大文件搜索变慢时先试 --no-mmap(本机实测 0.35s→0.24s);--trace 能证实当前走的是 memory map 还是 slice-by-line。
  3. 结果"变少"先怀疑忽略规则:默认尊重 .gitignore 但仅限 git 仓库内(本会话踩坑:非 git 目录 .gitignore 不生效,用 .rgignore 或 --no-require-git);-u/-uu/-uuu 逐级放开(等价 grep -r / grep -a -r)。
  4. -f patterns.txt 大模式文件变慢是 DFA 缓存太小:--dfa-size-limit 1G(FAQ 原文:"an internal cache is too small, and will cause ripgrep to spill over to a slower but more robust regular expression engine")。
  5. 需要 lookaround/反向引用才 -P(PCRE2);要榨干 PCRE2 速度用 -U --no-pcre2-unicode(FAQ 实测 2.962s→1.121s),代价是接受非 Unicode 语义。
  6. 高命中模式天然慢(README:"high match counts also tend to both tank performance"——本机 180 万命中实测 rg 明显掉速),能用 -l/--files-with-matches 短路就短路。
  7. 并行度用 -j 显式控制;共享一个 Regex 跨线程高频短干草垛有同步争用,文档建议 clone 后按线程分发。
  8. 编译一次、处处复用:std::sync::LazyLock 缓存 Regex,循环内重复编译是反模式(regex 文档明确列为 anti-pattern)。
  9. 管道组合:rg foo --files-with-matches -0 | xargs -0 sed -i 's/foo/bar/g'(FAQ 推荐做法;含空格路径用 NUL 分隔)。
  10. --json 输出给下游程序消费;--stats/--trace/--debug 三件套分别对应"性能仪表/内存策略/引擎策略"三个观测层。

与 grep 的分工:可移植脚本、POSIX 语义、任意环境 → grep;交互式代码搜索、大目录、Unicode → rg。FAQ 原文的表述值得记住:ripgrep "never was, isn't and never will be POSIX compatible"。


三、重要原文引述索引(防丢失)

主题引述出处
mmap 神话"Tools that search many files at once are generally *slower* if they use memory maps, not faster."博客开头三条声明
逐行搜索"Repeat after me: Thou Shalt Not Search Line By Line."博客 Mechanics 节
稀有字节"rg will actually try to guess the 'rarest' byte in a literal, and use memchr on that."subtitles_literal 分析
Teddy 出处"a special SIMD algorithm called Teddy, which was invented by Geoffrey Langdale as part of the Hyperscan regex library"linux_literal_casei 分析
UTF-8 in DFA"the regex engine builds UTF-8 decoding into its finite state machine… Invalid UTF-8 doesn't pose any problems"linux_unicode_word 分析
无锁队列"I was able to reuse a lock-free Chase-Lev work-stealing queue for distributing work to a pool of searchers"Gathering files 节
PCRE2 变慢"the first search will not search line by line, but the second one will… fast line searcher vs slow line searcher"FAQ #pcre2-slow
懒 DFA"usually by an order of magnitude on large haystacks"regex crate docs, perf-dfa
字面量加速上限"speedups of *several orders of magnitude*"regex crate docs, perf-literal
mmap 默认关"By default, memory maps are disabled."(MmapChoice::auto 才按文件大小/平台启发)grep-searcher docs, MmapChoice
O(m·n) 保证"all regex searches in this crate have worst case O(m * n) time complexity"regex crate docs 头部描述
迭代器例外"Regex::find_iter … actually have worst case O(m * n^2) search time"regex crate docs, Untrusted haystacks

四、本会话执行记录(可复现)

# 语料生成(python -I,synthetic)
# single/big.log: 300.0MB, 每~5001行1个"Sherlock Holmes" → 359 hits
# tree: src00..src59/{build,code}/f000..f049.log,每文件120行,每37行1个 ERR_SYS
# tree/.rgignore: "build/"(.gitignore 在非 git 目录不生效,实测已踩坑)

export TIMEFORMAT='%R'
bench() { local label="$1"; shift; local t; for i in 1 2 3; do
  t=$( { time "$@" >/dev/null 2>/dev/null; } 2>&1 ); echo "$label run$i: ${t}s"; done; }
rg --version          # ripgrep 15.1.0 (rev af60c2de9d) features:+pcre2
"/c/Program Files/Git/usr/bin/grep.exe" --version   # grep (GNU grep) 3.0
rg ERR_SYS tree --stats              # 47484000→23742000 bytes searched(rgignore 生效后)
rg 'Sherlock Holmes' single/big.log --trace 2>&1 | grep -i 'memory map'
rg --debug '[a-z]+fox[a-z]*|lazy\s+cat' tiny.txt 2>&1 | grep extracted

(实测数据表见"数据与证据"节;本笔记所有数字均出自上表与来源原文,无转引猜测。)

← 资料库今日播报 →