引擎性能对比的测试方法
正则性能测试最容易做错的地方是样本选择:只拿正常数据跑,所有引擎看起来都很快。真正的差异出现在失败路径——长串不匹配输入、深层嵌套、构造出的最坏情况样本上。做对比前先明确要回答的问题:是某条模式在最坏输入下的耗时上界,还是整体吞吐量,两者的测法与结论完全不同。
基本信息
| 必测样本 | 成功样本、失败样本、最坏情况样本(长非匹配串、深嵌套、超长单行) |
|---|---|
| 必分离的变量 | 模式编译耗时与匹配耗时;引擎通常带编译缓存,重复测量会命中缓存 |
| 推荐指标 | 分位数(p50、p95、p99)与随输入长度变化的曲线,而不是平均值 |
| 固定项 | 引擎与版本、编译选项、字符编码、机器负载,缺一项结论就不可比 |
落地步骤
- 先写清被测问题与判定标准,例如「万行日志中单行最坏耗时不超过 5 毫秒」
- 准备三组样本并固定下来,后续每次对比都用同一组数据,避免样本漂移掩盖变化
- 把编译与匹配分开计时,明确是否受编译缓存影响
- 每个用例预热后重复足够轮次,报告分位数与标准差,避免 JIT 与垃圾回收噪声
- 画输入长度与耗时的关系曲线,判断复杂度是线性还是指数级增长
- 记录环境信息与命令,结论要能被他人复现
基准测试写法示例
基准测试写法示例
# Python:分别测编译与匹配,重复轮次交给 timeit
python -m timeit -s "import re; p=re.compile(r'^(\\w+@\\w+\\.\\w+)$')" "p.search('a'*200 + '!')"
# shell:用 hyperfine 做多次运行并统计分布
hyperfine --warmup 3 --runs 20 'grep -cE "^[0-9]{4}-" big.log'
# 观察输入长度与耗时的关系:长度翻倍时耗时应近似翻倍,指数增长说明存在回溯爆炸
for n in 10 20 40 80; do echo -n "$n "; time_grep_with_input_of_length $n; done常见测法与偏差
| 测法 | 偏差来源 | 修正方式 |
|---|---|---|
| 只测成功样本 | 失败路径才是最慢路径 | 加入必然失败的对照样本 |
| 循环内重复编译 | 编译耗时被计入匹配 | 先编译一次,再单独测匹配 |
| 只报平均值 | 长尾被均值掩盖 | 报 p95 与 p99 |
| 单轮运行 | 预热与 JIT 影响巨大 | 预热后再跑多轮 |
| 单机单次对比 | 引擎版本差异被忽略 | 记录版本并固定环境 |
同一引擎的不同版本往往比不同引擎之间的差距更明显:底层库换代、JIT 开关、Unicode 支持变化都会影响耗时。做引擎对比时先把版本锁死,再讨论快慢。
带有 JIT 的引擎(PCRE 的 JIT、.NET 的编译选项、JS 引擎的优化管线)首次执行明显更慢,冷启动数据不能代表稳态表现,服务端场景应分别记录冷启动与稳态两组数字。