常用编程语言和库的正则表达式性能对比
Golang 测试代码首先是 Go 的代码。regexp2go 是个命令行工具它会把给定的正则翻译成等价的 Go 语言实现我们要先生成代码$ go install github.com/CAFxX/regexp2golatest$ regexp2go -pkg re -fn MatchInfoLine ‘“TSLA.*?”’ ‘re/info_line.go’生成的代码会在 re 这个包里。接着我们读入测试文件并比较标准库和生成代码package mainimport (“bufio”“fmt”“os”“regexp”“rtest/re”“time”)func matchWithRegexp(lines []string, re *regexp.Regexp) int64 {var matches int64 0for _, line : range lines {if re.MatchString(line) {matches}}return matches}func matchWithRegexp2go(lines []string) int64 {var matches int64 0matcher : 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#include#include#include#include#include#include#define PCRE2_CODE_UNIT_WIDTH 8#include pcre2.h// 1. std::regex 匹配函数long long match_with_std_regex(const std::vectorstd::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::vectorstd::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 JITlong long match_with_pcre2_jit(const std::vectorstd::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::vectorstd::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::durationdouble, 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::durationdouble, 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::durationdouble, 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 -stdc23 -Wall -O3 -lpcre2-8 a.cppPython 测试代码Python 虽然本身运行缓慢但它的标准库并不慢因此我选择它作为性能对比的基线import reimport timeimport sysdef match_with_re(lines, compiled_re):matches 0for line in lines:if compiled_re.search(line):matches 1return matchesdef main():filename “test.data”try: with open(filename, r, encodingutf-8) as f: lines f.read().splitlines() except FileNotFoundError: print(f无法打开文件: {filename}, filesys.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})ifname “main”:main()测试结果尽管只运行了两轮但每个函数匹配的次数都在 1000 万次以上足够摊平统计差异了。下面是测试结果实现 总耗时 单次扫描平均耗时Go regexp 1.771614875 s 885.8074 msGo regexp2go 1m27.618973125 s 43809.4866 msPython re 2038.25 ms 1019.1254 msstd::regex 20775.7 ms 10387.8 msPCRE2 4625.82 ms 2312.91 msPCRE2 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 做更多工作最终导致速度变慢。