对比一下我常用语言和库的正则匹配性能。
主要对比 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 语言实现,我们要先生成代码:
console
$ go install github.com/CAFxX/regexp2go@latest
$ regexp2go -pkg re -fn MatchInfoLine '\"TSLA.*?\"' > 're/info_line.go'
生成的代码会在 re 这个包里。接着我们读入测试文件,并比较标准库和生成代码:
go
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:
c++
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <chrono>
#include <regex>
#define PCRE2_CODE_UNIT_WIDTH 8
#include <pcre2.h>
// 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 虽然本身运行缓慢,但它的标准库并不慢,因此我选择它作为性能对比的基线:
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 并不比别家简单易用,性能更是拉胯,唯一的优点是不需要安装额外的依赖。