ripgrep benchsuite 实战:在 i9-12900K 上复现 2022-12-16 命令行搜索工具基准测试并读懂结果
【免费下载链接】ripgrepripgrep recursively searches directories for a regex pattern while respecting your gitignore项目地址: https://gitcode.com/GitHub_Trending/ri/ripgrep
ripgrep 仓库内置了一个名为 benchsuite 的基准测试框架,用于在真实语料上系统对比 ripgrep(rg)、GNU grep、ag、git grep、ugrep 等命令行搜索工具的性能与正确性。本文以仓库中 2022-12-16 在 Arch Linux(Intel i9-12900K、128GB 内存)上完成的基准运行记录为核心,完整还原当时的测试命令、工具版本与硬件环境,并结合 benchsuite 脚本 的源码讲清楚测试集如何设计、数据如何采集与汇总,帮助你独立复现这套测试、并正确解读 summary 中的每一组数字。
一、这次基准运行是什么:命令、环境与工具版本
本次运行的说明文档 记录了完整的采集方式。基准数据于 2022-12-16 采集,通过仓库根目录的benchsuite/benchsuite脚本运行,实际执行的命令是:
$ ./benchsuite \ --dir /dev/shm/benchsuite \ --raw runs/2022-12-16-archlinux-duff/raw.csv \ | tee runs/2022-12-16-archlinux-duff/summary这条命令有三个要点:
--dir /dev/shm/benchsuite:语料目录放在/dev/shm(tmpfs 内存盘)中,避免磁盘 I/O 波动干扰计时;--raw .../raw.csv:把每一次采样的原始数据(所有迭代、所有命令的耗时与命中行数)以 CSV 格式落盘,即 raw.csv;- 标准输出经
tee同时打印并保存为 summary,即按基准项分组、展示“均值 ± 标准差”的汇总表。
参与对比的工具版本(摘自说明文档):
| 工具 | 版本 | 备注 |
|---|---|---|
| ripgrep | 13.0.0 (rev 87c4a2b4b1) | 编译时未启用 SIMD/AVX(-SIMD -AVX (compiled)),运行时探测启用(+SIMD +AVX (runtime)) |
| GNU grep | 3.8 | |
| ag | 2.2.0 | +jit +lzma +zlib |
| git | 2.39.0 | 以git grep形式参与 |
| ugrep | 3.9.2 | +avx2 +pcre2jit +zlib +bzip2 +lzma +lz4 +zstd |
ripgrep 使用的二进制并非发行版预编译包,而是从 commit 7f23cd63 源码编译:
$ cargo build --release --features 'pcre2'关于pcre2feature,当前仓库的 Cargo.toml 中可见其定义:pcre2 = ["grep/pcre2"],即通过 crates/grep 启用可选依赖 crates/pcre2(一个封装 Rustpcre2crate 的 grep matcher)。这意味着基准测试里的 rg 支持 PCRE2 兼容语法,与很多发行版打包默认配置保持一致。
运行环境为 Arch Linux,硬件是 Intel i9-12900K 处理器、128GB 内存。目录名中的 “duff” 是这台机器的代号——仓库里更早一次 2020-10-14 的运行(frink,ripgrep 12.1.1、grep 3.4、git 2.28)见 benchsuite/runs/2020-10-14-archlinux-frink/README.md,可以跨机器、跨版本对照阅读。
二、benchsuite 脚本的工作机制
benchsuite/benchsuite 是一个纯 Python 脚本(#!/usr/bin/env python3,无第三方依赖),它定义了三个核心对象:
Benchmark:一组用于相互比较的命令;Command:单个被测命令(参数直接透传给subprocess.run,但 stdin/stdout/stderr 由框架统一接管);Result:一次基准运行的全部采样,负责统计均值与标准差。
命令行参数
脚本main()中的 argparse 定义(benchsuite/benchsuite)支持以下常用选项:
| 参数 | 作用 |
|---|---|
--dir PATH | 语料目录,也是所有搜索命令的工作目录(默认当前目录) |
--download CORPUS | 下载并准备语料后退出;可选all、linux、subtitles-en、subtitles-ru。注意all解压后总计约 13GB,且包含完整编译 Linux 内核 |
--raw PATH | 把所有采样原始数据写成 CSV |
--warmup-iter N | 每个命令正式采样前的预热次数(默认 1) |
--bench-iter N | 每个命令正式采样次数(默认 3) |
--allow-missing | 允许某些被测命令缺失时跳过对应项继续运行 |
--disabled a,b | 逗号分隔的命令名列表,直接跳过 |
--list | 只列出可用基准项名称 |
--force | 允许覆盖已存在的--raw输出文件 |
bench PAT | 位置参数,正则过滤只运行匹配的基准项 |
collect_benchmarks(benchsuite/benchsuite)通过扫描全局命名空间中以bench_开头的函数来动态发现基准项,因此新增一个基准只需要新增一个同名函数。缺语料的基准项会打印missing: ...提示并跳过;缺命令的基准项则提示可用--allow-missing降级运行。
语料准备
脚本定义了两大性质完全不同的语料(源码注释原文:一个是“少量大文件”,一个是“大量小文件”,两者在性能特征和相关性策略上差异巨大):
- linux 语料:浅克隆 Linux 内核源码树(
git clone --depth 1),随后执行make defconfig与make -j$(nproc)完整构建内核(download_linux)。构建会往仓库里产生大量编译产物,这些“垃圾文件”正是搜索工具默认应该跳过的对象。vmlinux二进制文件存在与否被用作语料就绪的标志。 - subtitles 语料:从 OPUS-OpenSubtitles 下载英、俄双语单语文本并解压;英文语料还会截取前 5500 万行生成
en.sample.txt,使英文基准的规模与俄语语料接近(download_subtitles_en)。
采样与统计
Benchmark.run()对每个命令先执行warmup_count次预热(本次运行为 1 次),再执行count次正式采样(本次运行为 3 次)(benchsuite/benchsuite)。每次采样记录两个量:
duration:命令墙钟耗时(秒,含小数毫秒);line_count:搜索结果输出的行数(通过统计 stdout 中\n计数得到)。
行数的作用不只是统计——它是正确性交叉验证:同一基准下各工具的行数应当一致(除非命令的语义本身不同,例如 ASCII 与 Unicode 的\w定义不同)。Result.distribution_for用statistics.mean和statistics.stdev计算每组采样的均值与标准差,即 summary 中0.084 +/- 0.002这类数字的来源。
raw.csv 的格式
--raw输出的 CSV 固定包含 8 个字段(benchsuite/benchsuite):
benchmark,warmup_iter,iter,name,command,duration,lines,env本次运行的 raw.csv 共 400 行(1 行表头 + 399 条采样),例如:
linux_literal_default,1,3,rg,rg PM_RESUME,0.08678817749023438,39, linux_literal_default,1,3,rg,rg PM_RESUME,0.08307123184204102,39, linux_literal_default,1,3,rg,rg PM_RESUME,0.08347964286804199,39, linux_literal_default,1,3,ag,ag PM_RESUME,0.2955434322357178,39,其中env字段记录该命令附加的环境变量(如LC_ALL=C),这让每条数据都能被完整复现。
三、基准用例设计:从“故意不公平”到“尽量公平”
所有 linux 系列基准都在构建好的内核源码树上执行;subtitles 系列针对单个大文本文件。下面按脚本源码逐个说明(函数定义见 benchsuite/benchsuite)。
linux 系列(语料:Linux 内核源码树)
| 基准名 | 模式 | 设计意图 |
|---|---|---|
linux_literal_default | PM_RESUME | 故意不公平:各工具都用默认参数。rg/ag/git grep 默认会做智能过滤(跳过隐藏/二进制文件、遵循 .gitignore),而 grep/ugrep 默认不做,所以后者必然搜更多文件。注释明确说它“pedagogically useful”——用来说明默认行为的差异 |
linux_literal | PM_RESUME | 尽量公平:所有工具使用“各工具都有的最小参数集”,如-n输出行号,统一不做忽略大小写 |
linux_literal_casei | PM_RESUME+-i | 忽略大小写的字面量搜索 |
linux_re_literal_suffix | [A-Z]+_RESUME | 字面量嵌在正则里,考察正则引擎处理“前缀正则 + 后缀字面量”的能力 |
linux_word | PM_RESUME+-w | 全词匹配(word boundary) |
linux_unicode_greek | \p{Greek} | Unicode 属性类匹配(仅 rg 与 ugrep 参与) |
linux_unicode_greek_casei | \p{Greek}+-i | 忽略大小写的 Unicode 属性类。源码注释直言“Only ripgrep gets this right (and it's still fast)” |
linux_unicode_word | \wAh | 考察\w的 Unicode 感知。注释指出只有 ripgrep 和设置了LC_ALL=en_US.UTF-8的 git grep 才是真的按 Unicode 处理,其余用 ASCII 语义 |
linux_no_literal | \w{5}\s+\w{5}\s+\w{5}\s+\w{5}\s+\w{5} | 不含任何字面量的正则,击败所有“字面量快捷优化”(memmem 预过滤等),注释承认“applicability is somewhat suspicious” |
linux_alternates/linux_alternates_casei | ERR_SYS\|PME_TURN_OFF\|LINK_REQ_RST\|CFG_BME_EVT | 短字面量 alternation,无公共前缀字节,考察多字面量优化 |
各工具在同一基准中的典型调用方式(以linux_literal为例):
rg -n PM_RESUME # rg rg -n --mmap PM_RESUME # rg (mmap) ag -s PM_RESUME # ag (mmap),ag 默认内存映射 git grep -I -n PM_RESUME # git grep,LC_ALL=C ugrep -r --ignore-files --no-hidden -I -n PM_RESUME ./ # ugrep可以看到脚本为每个工具精心选择了“等价能力”的参数:git grep加-I(跳过二进制)、ugrep加--ignore-files --no-hidden -I、ag加-s(静默)、rg 单独列出--mmap变体以便对照 mmap 与 read 两种读取策略。对 grep/git grep 还显式设置LC_ALL:脚本顶部注释解释,grep 的 locale 设置对性能影响巨大(GREP_ASCII = {'LC_ALL': 'C'}与GREP_UNICODE = {'LC_ALL': 'en_US.UTF-8'}),所以同一工具常以 ASCII/Unicode 两种环境成对出现。
subtitles 系列(语料:OpenSubtitles 大文本)
英文(en.sample.txt)与俄语(ru.txt,西里尔文)各 7 个基准,模式围绕“Sherlock Holmes / Шерлок Холмс”展开:
| 基准名 | 模式 | 考察点 |
|---|---|---|
*_literal | Sherlock Holmes/Шерлок Холмс | 纯字面量,含rg与rg (no mmap)对照 |
*_literal_casei | 同上 +-i | 忽略大小写 |
*_literal_word | 字面量 + 词边界 | 对俄语基准,脚本手工拼接(?-u:^|\W)...(?-u:$|\W)来模拟 ASCII 词边界,因为\b无法在禁用 Unicode 的模式里使用 |
*_alternate | 5 个人名的 alternation | 多字面量选择 |
*_alternate_casei | 同上 +-i | 多字面量 + 忽略大小写 |
*_surrounding_words | \w+\s+Holmes\s+\w+ | 含内部字面量的复杂正则 |
*_no_literal | \w{5}\s+\w{5}...(7 组) | 无字面量正则。注释特别说明:带 Unicode 支持的 grep 在 2 分钟时仍无完成迹象,被作者手动终止,故该基准只跑 ASCII 变体 |
俄语基准还有两处值得注意的“公平性补丁”(源码注释):ugrep 会错误地把全合法的 UTF-8 俄语语料识别为二进制文件,因此给 ugrep 加-a强制按文本处理——注释坦承这“technically gives it an edge”;ag (lines) (ASCII)之类的变体则反映 ag 只按 ASCII 解释\w,对西里尔文本匹配 0 行的事实会被 line count 如实记录。
四、summary 汇总格式与星号含义
主循环把每个基准的结果打印成如下格式(benchsuite/benchsuite):
<benchmark name> (pattern: <pattern>) ---------------------------------- <name padded> <mean> +/- <stdev> (lines: <n>)*[fastest]两个*含义不同:
- 名字后的
*:该命令产出了全场最快的单次采样(fastest_sample); - 行末的
*:该命令的均值最低(fastest_cmd,按分布判定)。
同一基准内各命令的(lines: N)应当一致;出现分歧往往意味着结果语义或正确性不同(见下文实例)。
五、2022-12-16 运行结果解读
以下数据全部来自 summary,单位秒。
内核语料上的字面量搜索(linux_literal_default,默认参数、故意不公平):
| 命令 | 均值 ± 标准差 | lines |
|---|---|---|
| rg | 0.084 ± 0.002 * | 39 |
| ugrep | 0.105 ± 0.002 | 39 |
| git grep | 0.225 ± 0.007 | 39 |
| ag | 0.295 ± 0.001 | 39 |
| grep | 0.996 ± 0.003 | 39 |
公平参数版(linux_literal)中 rg 与自身 mmap 变体的对照:
| 命令 | 均值 ± 标准差 |
|---|---|
| rg | 0.085 ± 0.001 |
| rg (mmap) | 0.322 ± 0.002 |
| ag (mmap) | 0.290 ± 0.002 |
| git grep | 0.211 ± 0.009 |
| ugrep | 0.189 ± 0.005 |
一个直观结论:在此机器与语料上,rg默认(read 路径)显著快于显式--mmap,而 ag 因默认即 mmap,其速度与 rg 的 mmap 变体相近。
Unicode 相关基准最能体现引擎差异。linux_unicode_word(模式\wAh)中,git grep(Unicode 环境)耗时 3.980s(±0.241),而 rg 仅 0.085s;linux_no_literal中差距更悬殊:
| 命令 | 均值 ± 标准差 | lines |
|---|---|---|
| rg (ASCII) | 0.200 ± 0.001 | 720 |
| ugrep (ASCII) | 0.236 ± 0.003 | 722 |
| rg | 0.266 ± 0.006 | 721 |
| ugrep | 3.403 ± 0.008 | 723 |
| git grep (ASCII) | 2.144 ± 0.014 | 720 |
| git grep | 7.346 ± 0.017 | 721 |
| ag (ASCII) | 0.832 ± 0.007 | 1134 |
注意ag (ASCII)的 lines 为 1134,与其他工具约 720 不一致——ASCII 语义的\w与 Unicode 语义匹配的是不同集合,line count 把这种语义差异直接暴露了出来。
大文件语料上的表现:
| 基准(模式) | rg | 最慢者 |
|---|---|---|
subtitles_en_literal(Sherlock Holmes) | 0.123 | ag (lines) 1.868 |
subtitles_ru_literal(Шерлок Холмс) | 0.133 | ag (lines) 1.973 |
subtitles_en_surrounding_words(\w+\s+Holmes\s+\w+) | 0.200 | ugrep 43.220 |
subtitles_ru_no_literal(7 组\w{5}) | 2.236 | ugrep 28.811 |
subtitles_en_surrounding_words中 ugrep 耗时 43.220s(±0.047),rg 仅 0.200s(±0.001),是该次运行中最极端的单项差距;俄语同型基准中 ugrep 亦为 42.919s。而subtitles_ru_alternate_casei则展示了另一面:ag (ASCII) 以 2.727s 拿下全场最快(rg 为 3.673s),但 rg 的 lines 为 735、ag 为 691——Unicode 忽略大小写使 rg 多匹配了部分西里尔词形,说明“最快”与“匹配最完整”未必是同一个命令,这正是 line count 校验存在的意义。类似地,linux_unicode_greek_casei中 ugrep 的 lines(105)与 rg(245)不一致,对照脚本中“Only ripgrep gets this right”的注释,可以推断 ugrep 在该用例上未能正确完成 Unicode 忽略大小写匹配。
六、如何复现这套基准
完整复现步骤(在 ripgrep 仓库根目录下):
安装 Python 3、git、GNU grep、ag、ugrep,以及待测的 rg(建议按本次运行的方式从源码构建):
cargo build --release --features 'pcre2'下载并准备语料(约 13GB,且需要编译整个 Linux 内核,耗时较长):
./benchsuite --dir /dev/shm/benchsuite --download all该命令是幂等的;也可以只准备部分语料,如
--download linux。运行全部基准并落盘原始数据:
./benchsuite \ --dir /dev/shm/benchsuite \ --raw runs/<date>-<machine>/raw.csv \ | tee runs/<date>-<machine>/summary常用变体:
./benchsuite --list # 列出全部基准项 ./benchsuite 'unicode' # 只运行名称匹配 unicode 的基准 ./benchsuite --disabled grep,ag # 跳过指定命令 ./benchsuite --warmup-iter 1 --bench-iter 5 # 提高采样精度
两点实践提示:其一,把--dir指向 tmpfs(如本次运行的/dev/shm/benchsuite)可显著降低 I/O 噪声,前提是内存装得下语料;其二,--raw已存在时会拒绝覆盖(--force可解除),避免误覆盖历史数据。若想对照历史结果,仓库中还保存了 2016、2018、2020 年的多组运行(如 benchsuite/runs/2016-12-24-archlinux-cheetah/summary),但注意 CPU 代际不同,跨机器只宜看量级不宜看绝对值。
七、如何正确看待这些数字
最后给出阅读这份(以及仓库中任何一次)benchsuite 运行记录时的适用前提与限制:
- 单机结果:所有数据只在“i9-12900K + 128GB + Arch Linux + tmpfs”这一组合下成立,工具版本(rg 13.0.0、grep 3.8、ag 2.2.0、git 2.39.0、ugrep 3.9.2)也是数据的一部分,换机器或换版本都可能改变排序;
- 语料依赖:linux 语料是“大量小文件 + 编译垃圾”,subtitles 语料是“少量大文件”,两类语料考察的策略完全不同(文件遍历/过滤 vs 单文件内存搜索),不能由一类结果外推另一类;
- 正确性优先于速度:同一基准中 lines 不一致说明语义或匹配正确性存在差异(ASCII/Unicode 词边界、二进制识别、Unicode case-folding),此时“快”没有意义;
- 公平性是有代价的:
linux_literal_default是故意不公平的对照实验;而“公平”版本也只能对齐各工具能力的交集,脚本注释多次承认这种对齐的局限(如给 ugrep 加-a的补偿)。
理解以上边界后,这份 2022-12-16 的运行记录就不仅是一组静态数字,而是一套可复现、可验证、可继续扩展(新增bench_*函数即可加入基准项)的搜索工具评测方法论的完整实例。
【免费下载链接】ripgrepripgrep recursively searches directories for a regex pattern while respecting your gitignore项目地址: https://gitcode.com/GitHub_Trending/ri/ripgrep
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考