常用编程语言和库的正则表达式性能对比
作者:@apocelipes
本文为作者原创,转载请注明出处:https://www.cnblogs.com/apocelipes/p/21055314
对比一下我常用语言和库的正则匹配性能。
主要对比 C++ 标准库、PCRE2、Golang 标准库和 Python 标准库。此外还有一个 Golang 的代码生成方案,它可以把给定的正则表达式编译成 Go 代码:regexp2go
测试环境
测试用的编程语言和库版本:
- Python: 3.14.6
- GCC: 16.1.1
- C++ standard: C++23
- PCRE2: 10.47
- Golang: 1.26.4
- regexp2go: master
测试运行的系统:
- OS: macOS 26
- RAM: 32G
测试数据:
一个记录了热门股票交易动态的 JSON 数据文件,大概有 500 万行,大小约 1.5G,数据按 JSON string 的形式组织。具体数据不方便发上来,自己构建一个结构类似的数据也可以实现同样的测试效果。
Golang 测试代码
首先是 Go 的代码。regexp2go 是个命令行工具,它会把给定的正则翻译成等价的 Go 语言实现,我们要先生成代码:
生成的代码会在 re 这个包里。接着我们读入测试文件,并比较标准库和生成代码:
| package main |
| import ( |
| "bufio" |
| "fmt" |
| "os" |
| "regexp" |
| "rtest/re" |
| "time" |
| ) |
| func matchWithRegexp(lines []string, re *regexp.Regexp) int64 { |
| var matches int64 = 0 |
| for _, line := range lines { |
| if re.MatchString(line) { |
| matches++ |
| } |
| } |
| return matches |
| } |
| func matchWithRegexp2go(lines []string) int64 { |
| var matches int64 = 0 |
| matcher := re.MatchInfoLine{} |
| for _, line := range lines { |
| _, _, ok := matcher.FindString(line) |
| if ok { |
| matches++ |
| } |
| } |
| return matches |
| } |
| func main() { |
| filename := "test.data" |
| file, err := os.Open(filename) |
| if err != nil { |
| fmt.Printf("无法打开文件: %v\n", err) |
| return |
| } |
| defer file.Close() |
| var lines []string |
| scanner := bufio.NewScanner(file) |
| for scanner.Scan() { |
| lines = append(lines, scanner.Text()) |
| } |
| if err := scanner.Err(); err != nil { |
| fmt.Printf("读取文件出错: %v\n", err) |
| return |
| } |
| fmt.Printf("文件读取完成。总行数: %d\n\n", len(lines)) |
| if len(lines) == 0 { |
| return |
| } |
| pattern := `\"TSLA.*?\"` |
| re, err := regexp.Compile(pattern) |
| if err != nil { |
| fmt.Printf("正则编译失败: %v\n", err) |
| return |
| } |
| const iterations = 2 |
| { |
| var totalMatches int64 |
| start := time.Now() |
| for range iterations { |
| totalMatches += matchWithRegexp(lines, re) |
| } |
| totalDuration := time.Since(start) |
| avgDurationNs := totalDuration.Nanoseconds() / int64(iterations) |
| avgDurationMs := float64(avgDurationNs) / 1e6 |
| fmt.Printf("[Go regexp 结果] -------\n") |
| fmt.Printf("循环总次数: %d\n", iterations) |
| fmt.Printf("总耗时: %v\n", totalDuration) |
| fmt.Printf("单次扫描平均耗时: %.4f ms\n", avgDurationMs) |
| fmt.Printf("累计匹配成功行数: %d\n", totalMatches) |
| } |
| { |
| var totalMatches int64 |
| start := time.Now() |
| for range iterations { |
| totalMatches += matchWithRegexp2go(lines) |
| } |
| totalDuration := time.Since(start) |
| avgDurationNs := totalDuration.Nanoseconds() / int64(iterations) |
| avgDurationMs := float64(avgDurationNs) / 1e6 |
| fmt.Printf("\n[Go regexp2go 结果] -------\n") |
| fmt.Printf("循环总次数: %d\n", iterations) |
| fmt.Printf("总耗时: %v\n", totalDuration) |
| fmt.Printf("单次扫描平均耗时: %.4f ms\n", avgDurationMs) |
| fmt.Printf("累计匹配成功行数: %d\n", totalMatches) |
| } |
| } |
我选择测试的正则是 \"TSLA.*?\",这种正则在业务中很常用。测试时把文件内容全部读入内存,以免结果受到 I/O 的干扰。
C++ 测试代码
C++ 的测试也一样:先读入文件内容,然后分别测试标准库和 PCRE2:
| // 1. std::regex 匹配函数 |
| long long match_with_std_regex(const std::vector<std::string>& lines, const std::regex& txt_regex) { |
| long long matches = 0; |
| for (const auto& line : lines) { |
| if (std::regex_search(line, txt_regex)) { |
| matches++; |
| } |
| } |
| return matches; |
| } |
| // 2. PCRE2 匹配函数 |
| long long match_with_pcre2(const std::vector<std::string>& lines, pcre2_code* re, pcre2_match_data* match_data) { |
| long long matches = 0; |
| for (const auto& line : lines) { |
| int rc = pcre2_match( |
| re, |
| (PCRE2_SPTR)line.c_str(), |
| line.length(), |
| 0, 0, |
| match_data, |
| NULL |
| ); |
| if (rc >= 0) { |
| matches++; |
| } |
| } |
| return matches; |
| } |
| // 3. PCRE2 JIT |
| long long match_with_pcre2_jit(const std::vector<std::string>& lines, pcre2_code* re, pcre2_match_data* match_data) { |
| long long matches = 0; |
| for (const auto& line : lines) { |
| int rc = pcre2_jit_match( |
| re, |
| (PCRE2_SPTR)line.c_str(), |
| line.length(), |
| 0, 0, |
| match_data, |
| NULL |
| ); |
| if (rc >= 0) { |
| matches++; |
| } |
| } |
| return matches; |
| } |
| int main() { |
| std::string filename = "test.data"; |
| std::ifstream infile(filename); |
| if (!infile.is_open()) { |
| std::cerr << "无法打开文件: " << filename << std::endl; |
| return 1; |
| } |
| std::vector<std::string> lines; |
| std::string line; |
| while (std::getline(infile, line)) { |
| lines.push_back(line); |
| } |
| infile.close(); |
| std::cout << "文件读取完成。总行数: " << lines.size() << "\n\n"; |
| if (lines.empty()) return 0; |
| const int ITERATIONS = 2; |
| // ------------------ 1. std::regex 测试 ------------------ |
| { |
| std::regex txt_regex(R"(\"TSLA.*?\")"); |
| long long total_matches = 0; |
| auto start = std::chrono::high_resolution_clock::now(); |
| for (int i = 0; i < ITERATIONS; ++i) { |
| total_matches += match_with_std_regex(lines, txt_regex); |
| } |
| auto end = std::chrono::high_resolution_clock::now(); |
| std::chrono::duration<double, std::milli> total_duration = end - start; |
| std::cout << "[std::regex 结果] -------\n" |
| << "循环总次数: " << ITERATIONS << "\n" |
| << "总耗时: " << total_duration.count() << " ms\n" |
| << "单次扫描平均耗时: " << (total_duration.count() / ITERATIONS) << " ms\n" |
| << "累计匹配成功行数: " << total_matches << "\n\n"; |
| } |
| // ------------------ 2. PCRE2 测试 ------------------ |
| { |
| int errorcode; |
| PCRE2_SIZE erroroffset; |
| PCRE2_SPTR pattern = (PCRE2_SPTR)"\\\"TSLA.*?\\\""; |
| pcre2_code* re = pcre2_compile(pattern, PCRE2_ZERO_TERMINATED, 0, &errorcode, &erroroffset, NULL); |
| if (re == NULL) { |
| std::cerr << "PCRE2 编译失败\n"; |
| return 1; |
| } |
| pcre2_match_data* match_data = pcre2_match_data_create_from_pattern(re, NULL); |
| long long total_matches = 0; |
| auto start = std::chrono::high_resolution_clock::now(); |
| for (int i = 0; i < ITERATIONS; ++i) { |
| total_matches += match_with_pcre2(lines, re, match_data); |
| } |
| auto end = std::chrono::high_resolution_clock::now(); |
| std::chrono::duration<double, std::milli> total_duration = end - start; |
| pcre2_match_data_free(match_data); |
| pcre2_code_free(re); |
| std::cout << "[PCRE2 结果] ------------\n" |
| << "循环总次数: " << ITERATIONS << "\n" |
| << "总耗时: " << total_duration.count() << " ms\n" |
| << "单次扫描平均耗时: " << (total_duration.count() / ITERATIONS) << " ms\n" |
| << "累计匹配成功行数: " << total_matches << "\n\n"; |
| } |
| // ------------------ 3. PCRE2 JIT 测试 ------------------ |
| { |
| int errorcode; |
| PCRE2_SIZE erroroffset; |
| PCRE2_SPTR pattern = (PCRE2_SPTR)"\\\"TSLA.*?\\\""; |
| pcre2_code* re = pcre2_compile(pattern, PCRE2_ZERO_TERMINATED, 0, &errorcode, &erroroffset, NULL); |
| if (re == NULL) { |
| std::cerr << "PCRE2 编译失败\n"; |
| return 1; |
| } |
| int jit_rc = pcre2_jit_compile(re, PCRE2_JIT_COMPLETE); |
| if (jit_rc < 0) { |
| // 返回负数说明当前平台或环境不支持 JIT(例如某些高安全性系统禁用了内存执行权限) |
| // 此时它会退回到普通的非 JIT 模式,代码依然能跑,但速度会慢 |
| std::cout << "当前环境不支持 JIT 编译,退回到普通模式\n"; |
| } else { |
| std::cout << "PCRE2 JIT 编译成功启用!\n"; |
| } |
| pcre2_match_data* match_data = pcre2_match_data_create_from_pattern(re, NULL); |
| long long total_matches = 0; |
| auto start = std::chrono::high_resolution_clock::now(); |
| for (int i = 0; i < ITERATIONS; ++i) { |
| total_matches += match_with_pcre2(lines, re, match_data); |
| } |
| auto end = std::chrono::high_resolution_clock::now(); |
| std::chrono::duration<double, std::milli> total_duration = end - start; |
| pcre2_match_data_free(match_data); |
| pcre2_code_free(re); |
| std::cout << "[PCRE2 JIT 结果] ------------\n" |
| << "循环总次数: " << ITERATIONS << "\n" |
| << "总耗时: " << total_duration.count() << " ms\n" |
| << "单次扫描平均耗时: " << (total_duration.count() / ITERATIONS) << " ms\n" |
| << "累计匹配成功行数: " << total_matches << "\n\n"; |
| } |
| return 0; |
| } |
C++ 还顺带测试了 PCRE2 的 JIT 编译器,它支持几乎所有常见 CPU 平台,甚至包括龙芯,并且可以极大提高匹配性能。PCRE2 里我还开启了 Unicode 字符处理,在这个场景里这是必要的;开启这个处理会造成一点轻微的性能下降。
编译命令是 g++ -std=c++23 -Wall -O3 -lpcre2-8 a.cpp
Python 测试代码
Python 虽然本身运行缓慢,但它的标准库并不慢,因此我选择它作为性能对比的基线:
| import re |
| import time |
| import sys |
| def match_with_re(lines, compiled_re): |
| matches = 0 |
| for line in lines: |
| if compiled_re.search(line): |
| matches += 1 |
| return matches |
| def main(): |
| filename = "test.data" |
| try: |
| with open(filename, "r", encoding="utf-8") as f: |
| lines = f.read().splitlines() |
| except FileNotFoundError: |
| print(f"无法打开文件: {filename}", file=sys.stderr) |
| return |
| print(f"文件读取完成。总行数: {len(lines)}\n") |
| if not lines: |
| return |
| pattern = r'\"TSLA.*?\"' |
| compiled_re = re.compile(pattern) |
| iterations = 2 |
| total_matches = 0 |
| start_time = time.perf_counter() |
| for _ in range(iterations): |
| total_matches += match_with_re(lines, compiled_re) |
| end_time = time.perf_counter() |
| total_duration_secs = end_time - start_time |
| total_duration_ms = total_duration_secs * 1000 |
| avg_duration_ms = total_duration_ms / iterations |
| print(f"[Python re 结果] -------") |
| print(f"循环总次数: {iterations}") |
| print(f"总耗时: {total_duration_ms:.2f} ms") |
| print(f"单次扫描平均耗时: {avg_duration_ms:.4f} ms") |
| print(f"累计匹配成功行数: {total_matches}") |
| if __name__ == "__main__": |
| main() |
测试结果
尽管只运行了两轮,但每个函数匹配的次数都在 1000 万次以上,足够摊平统计差异了。
下面是测试结果:
| 实现 | 总耗时 | 单次扫描平均耗时 |
|---|---|---|
| Go regexp | 1.771614875 s | 885.8074 ms |
| Go regexp2go | 1m27.618973125 s | 43809.4866 ms |
| Python re | 2038.25 ms | 1019.1254 ms |
| std::regex | 20775.7 ms | 10387.8 ms |
| PCRE2 | 4625.82 ms | 2312.91 ms |
| PCRE2 JIT | 238.041 ms | 119.02 ms |
| 实现 | 总耗时倍率 | 单次扫描平均耗时倍率 |
|---|---|---|
| Go regexp | 0.87× | 0.87× |
| Go regexp2go | 43.02× | 42.99× |
| Python re | 1.00× | 1.00× |
| std::regex | 10.19× | 10.19× |
| PCRE2 | 2.27× | 2.27× |
| PCRE2 JIT | 0.12× | 0.12× |
从结果来看,regexp2go 是最慢的。这个工具号称比 Go 1.16 的标准库最多快 5 倍,要么是 Go 的标准库有了飞跃式提升,要么是它夸大宣传。
C++ 标准库的 regex 慢是众所周知的,不过没想到会比 Python 基线慢一个数量级,令人捧腹。不同的标准库实现之间性能也是天差地别,我选用了最快的 libstdc++;如果换成 LLVM 的 libc++,性能会回退到和 regexp2go 一桌。除了慢之外,标准库还有代码膨胀的问题,仅仅简单使用基础功能和一个不算复杂的模式,就产生了 200 KB 左右的编译产物。
令人意外的是,PCRE2 在未开启 JIT 时居然会比 Python 慢。这是因为对于非贪婪匹配,PCRE2 的引擎会比 Go 和 Python 做更多工作,最终导致速度变慢。
总结
如果追求极致性能,PCRE2 的 JIT 模式是最佳选择。不过要注意,JIT 会占用更多内存,并且在一些安全要求很高的环境中无法正常运行。
否则 Golang 和 Python 都是够用的,尤其考虑到现在的 AI 时代,选这两门语言做一些简单工具再合适不过了。
最后,永远不要主动去用 C++ 标准库提供的 regex。它的 API 并不比别家简单易用,性能更是拉胯,唯一的优点是不需要安装额外的依赖。
浙公网安备 33010602011771号