grep -o 在一行 160 万字符的文件上跑了 14 秒:一次正则回溯排查

举报
yd_268799136 发表于 2026/10/25 11:21:59 2026/10/25
【摘要】 同一份 1.9MB 文件,grep -c 秒回、grep -o 却要 14 秒。七组实测把行长、窗口宽度、locale 三个变量拆开,最反直觉的一条是输出到 /dev/null 就不慢。

线上排查时遇到过一次很反直觉的性能问题:同样一个 1.9MB 的压缩 JS 文件,grep -c 秒回,grep -bo 也是毫秒级,换成 grep -oE '.{0,40}关键字.{0,40}' 却要等十几秒才出第一行。机器不慢,磁盘不慢,慢的是正则本身。

现象

目标是从一份构建产物里把某个字符串前后的上下文捞出来。文件 1.9MB,只有两行——压缩后的 Next.js chunk 就是这样,一行一百六十万字符,另一行三十万。

命令是常见的那一套:

grep -oE '.{0,40}TARGETKEYWORD.{0,40}' bundle.js

结果是光标闪了十几秒,什么都没打印。第一反应是 IO 或者内存,但同一个文件用别的命令都很快,问题不在文件。

把变量拆开

为了排除干扰,我构造了一个可复现的最小样本:一个 1.9MB 的文件,一行 160 万字符,关键字埋在偏移 120 万的位置。然后逐项对比。

命令 耗时
grep -oE '.{0,40}KW.{0,40}' 输出到文件 14285 ms
同上,输出到 /dev/null 24 ms
LC_ALL=C grep -oE '.{0,40}KW.{0,40}' 524 ms
rg -o '.{0,40}KW.{0,40}' 21 ms
grep -boE 'KW' 17 ms
grep -oE 'KW'(不带窗口) 16 ms
同样 1.9MB 但切成 3.6 万短行,带窗口 13 ms

七组数据把三个变量摘了出来。

行长是第一位的。 文件总大小一样,切成短行之后 13ms 就出来了,比原来快三个数量级。也就是说代价跟「单行长度」有关,跟文件大小基本无关。

-o 的窗口是第二个。 同一个长行,去掉 .{0,40} 只要 16ms。窗口让匹配的起点可以在 40 个不同位置上,而 -o 要求输出的是完整匹配文本,不是「这一行命中了」这种布尔值。

locale 是第三个。 加 LC_ALL=C 从 14 秒降到 524ms。

最反直觉的一条:输出到 /dev/null 就不慢

这组数据里最值得说的是第二行。同一条命令,唯一的区别是把 stdout 接到 /dev/null,耗时从 14285ms 掉到 24ms,快了将近 600 倍。

原因不难理解:结果没人要的时候,算匹配边界这件事就没有意义。-o 的昂贵部分恰恰在于「这个匹配到底从哪开始、到哪结束」,而不是「这一行有没有命中」。命中判定由 DFA 线性扫一遍就够,边界计算则要围绕每个候选起点反复回退试探。

这个特性有个副作用:用 >/dev/null 来测速会得出完全相反的结论。我曾经在压测里加过 >/dev/null,六秒多的命令看起来只要 45 秒之外的另一个数量级,结论直接翻转。测 grep 性能时不要为了「不让它刷屏」而丢掉输出,日志落文件再统计才是真的。

根因

-o 的语义是「只输出匹配到的部分」,实现上必须为每一个匹配算出精确的起止位置。.{0,40} 这种有界重复给了匹配 40 个可能的起点,最左最长的语义要求把这 40 个候选都试过才能确定哪一个最长。在一行 160 万字符上,这个「试」的成本随行长线性放大,再乘上窗口宽度。

多字节 locale 又在这上面叠了一层:UTF-8 下每个字节都要参与「是否合法多字节序列起始」的判定,字符类被展开成字节区间,. 也不等于一个字节。这也是为什么 LC_ALL=C 能把时间砍掉 96%——它把匹配退化成纯字节比较。

处理

四条路子,按推荐顺序:

一、换 ripgrep。 rg -o 在这份样本上 21ms,比 GNU grep 快 680 倍。ripgrep 默认按行流式处理,正则引擎对长行也友好,日常检索直接替代 grep 就行。

二、加 LC_ALL=C。 不改命令、不装新工具,524ms 已经可用。前提是关键字是纯 ASCII,涉及中文匹配时不能这么用。

三、用字节偏移再切片。 grep -boE 'KW' 只输出偏移量,17ms 拿到位置,再用 dd 从偏移附近切一段出来看上下文。适合不想引入新依赖、又需要精确位置的场景。

四、先还原源码。 压缩产物有 source map 的话直接还原,没有就先 npx js-beautify 展开再按行 grep。治标但有效,尤其是要反复读同一份产物的时候。

结论

  • grep -o 慢在算匹配边界,不在扫文件;行长和窗口宽度是主要变量
  • 同一份文件,grep -c 快不代表 grep -o 也快
  • LC_ALL=C 能砍掉九成以上开销,rg 能砍掉三个数量级
  • 不要用 >/dev/null 测 grep 性能,它会跳过最贵的那部分

顺带一条通用经验:所有可能跑很久的命令都套一层 timeout。这次排查里我有一次十分钟没等到输出,进程还挂着占 CPU,加超时之后再出问题至少不会拖住整个会话。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。