常用编程语言和库的正则表达式性能对比

对比一下我常用语言和库的正则匹配性能。

主要对比 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 并不比别家简单易用,性能更是拉胯,唯一的优点是不需要安装额外的依赖。

相关推荐
用户8356290780513 小时前
使用 Python 在 PDF 中创建与管理书签
后端·python
MeixianAgent7 小时前
Python 回测数据入口怎么验?历史 K 线入库前先做 5 个检查
后端·python
咕白m62511 小时前
用 Python 实现一键批量查找与替换 Excel 数据
后端·python
SelectDB1 天前
Apache Doris Python UDF:让 SQL 直接调用 Python 生态,支撑 Agent 时代复杂业务逻辑
大数据·数据库·python
郝学胜_神的一滴1 天前
CMake 034:生成器表达式:解耦构建时序、精简分支逻辑的终极利器
c++·cmake
荣码1 天前
GraphRAG:普通RAG只能回答"点"的问题,我踩了4个坑才搞懂
java·python
金銀銅鐵2 天前
[Python] 基于欧几里得算法,实现分数约分计算器
python·数学
Lyn_Li2 天前
Kaggle Top 5 | 198只股票、200条数据的金融预测——BattleFin高分方案从零复现
python·kaggle·比赛复盘·金融预测
见过夏天2 天前
C++ 基础入门完全指南
c++