提高c++正则表达式替换性能

Increase C++ regex replace performance

本文关键字:性能 替换 正则表达式 c++ 提高      更新时间:2023-10-16

我是一个c++编程初学者,在一个小型c++项目上工作,我必须处理许多相对较大的XML文件并从中删除XML标记。我使用c++ 0x正则表达式库成功地做到了这一点。但是,我遇到了一些性能问题。只是读取文件并在其内容上执行regex_replace函数在我的PC上大约需要6秒。我可以通过添加一些编译器优化标志将其减少到2。然而,使用Python,我可以在不到100毫秒的时间内完成。显然,我在c++代码中做了一些非常低效的事情。我怎么做才能加快速度呢?

我的c++代码:

std::regex xml_tags_regex("<[^>]*>");
for (std::vector<std::string>::iterator it = _files.begin(); it != 
_files.end(); it++) {
std::ifstream file(*it);
file.seekg(0, std::ios::end);
size_t size = file.tellg();
std::string buffer(size, ' ');
file.seekg(0);
file.read(&buffer[0], size);
buffer = regex_replace(buffer, xml_tags_regex, "");
file.close();
}

My Python code:

regex = re.compile('<[^>]*>')
for filename in filenames:
with open(filename) as f:
content = f.read()
content = regex.sub('', content)

注:我真的不关心一次处理完整的文件。我只是发现一行一行、一个字一个字或一个字一个字地阅读文件会大大减慢速度。

c++ 11正则表达式替换确实相当慢,至少到目前为止是这样。PCRE在模式匹配速度方面表现得更好,然而,PCRECPP为基于正则表达式的替换提供了非常有限的方法,引用手册页:

你可以用"rewrite"替换"str"中"pattern"的第一个匹配。在"rewrite"中,反斜杠转义的数字(1到9)可用于中插入与圆括号组相匹配的文本模式。"rewrite"中的是指整个匹配的文本。

与Perl的's'命令相比,这真的很糟糕。这就是为什么我围绕PCRE编写了自己的c++包装器,它以一种接近Perl's '的方式处理基于正则表达式的替换,并且还支持16位和32位字符串:

命令字符串语法

命令语法遵循Perls/pattern/substitute/[options]公约。任何字符(除了反斜杠)都可以用作分隔符,不仅仅是/,但要确保分隔符被转义为如果在pattern,substituteoptions中使用反斜杠()子字符串,例如:

  • s/\///g将所有反斜杠替换为正斜杠

记住在c++代码中使用双反斜杠,除非使用原始字符串文字(参见字符串文字):

pcrscpp::replace rx("s/\\/\//g");

模式字符串语法

模式字符串直接传递给pcre*_compile,因此必须按照PCRE文档中描述的PCRE语法。

替换字符串语法

替代字符串反向引用语法类似于Perl的:

  • $1$n:第n个捕获子模式匹配
  • $&$0:整个匹配
  • ${label}:标记子模式匹配。label最多为32个字母数字+下划线字符('A'-'Z','a'-'z','0'-'9','_'),第一个字符必须是字母
  • $`$'(反勾号和勾号)是指前面的主题区域和比赛结束后。在Perl中,未修改的

也可以识别以下转义序列:

  • n: newline
  • r:回车
  • t:水平标签
  • f: form feed
  • b: backspace
  • a: alarm, bell
  • e: escape
  • :二进制零

其他转义序列<char>被解释为<char>,这意味着你也必须转义反斜杠

选项字符串语法

在类似perl的方式中,选项字符串是一个允许的修饰符序列信件。PCRSCPP识别以下修饰语:

  1. perl的旗帜
    • g:全局替换,而不仅仅是第一个匹配
    • i:大小写不敏感匹配
      (PCRE_CASELESS)
    • m:多行模式:^$另外匹配位置
      (PCRE_MULTILINE)
    • s:允许.元字符的作用域包括换行符(将换行符视为普通字符)
      (PCRE_DOTALL)
    • x:允许扩展正则表达式语法在复杂模式中启用空白和注释
      (PCRE_EXTENDED)
  2. PHP-compatible旗帜
    • A: "anchor"模式:只查找"锚定"匹配从零偏移开始。在单行模式下等于以^
      (pcre_anchor)
    • 作为所有模式可选分支的前缀
    • D:只将$$作为主题结束断言,覆盖默认值:结束,或紧接在末尾换行符之前。忽略多行模式
      (PCRE_DOLLAR_ENDONLY)
    • U:反转*+贪婪逻辑:默认为不贪婪;?切换回贪心模式。(?U)(?-U)模式开关不受影响
      (PCRE_UNGREEDY)
    • u: Unicode模式。将模式和主题处理为UTF8/UTF16/UTF32字符串。不像在PHP,也影响换行,R,d,w等匹配
      ((PCRE_UTF8/PCRE_UTF16/PCRE_UTF32) | PCRE_NEWLINE_ANY| pcre_bsr_unicode | pcre_ucp)
  3. PCRSCPP自己的标志:
    • N:跳过空匹配
      (PCRE_NOTEMPTY)
    • T:将substitute视为普通字符串,即不进行反向引用和转义序列解释
    • n:丢弃字符串的不匹配部分来替换
      注意:PCRSCPP不自动添加换行,替换结果是匹配的简单串联,在多行模式下要特别注意这一点

我写了一个简单的速度测试代码,它存储了文件"move.sh"的10倍副本,并对结果字符串测试regex性能:

#include <pcrscpp.h>
#include <string>
#include <iostream>
#include <fstream>
#include <regex>
#include <chrono>
int main (int argc, char *argv[]) {
const std::string file_name("move.sh");
pcrscpp::replace pcrscpp_rx(R"del(s/(?:^|n)mv[ t]+(?:-f)?[ t]+"([^n]+)"[ t]+"([^n]+)"(?:$|n)/$1n$2n/Dgn)del");
std::regex std_rx          (R"del((?:^|n)mv[ t]+(?:-f)?[ t]+"([^n]+)"[ t]+"([^n]+)"(?:$|n))del");
std::ifstream file (file_name);
if (!file.is_open ()) {
std::cerr << "Unable to open file " << file_name << std::endl;
return 1;
}
std::string buffer;
{
file.seekg(0, std::ios::end);
size_t size = file.tellg();
file.seekg(0);
if (size > 0) {
buffer.resize(size);
file.read(&buffer[0], size);
buffer.resize(size - 1); // strip ''
}
}
file.close();
std::string bigstring;
bigstring.reserve(10*buffer.size());
for (std::string::size_type i = 0; i < 10; i++)
bigstring.append(buffer);
int n = 10;
std::cout << "Running tests " << n << " times: be patient..." << std::endl;
std::chrono::high_resolution_clock::duration std_regex_duration, pcrscpp_duration;
std::chrono::high_resolution_clock::time_point t1, t2;
std::string result1, result2;
for (int i = 0; i < n; i++) {
// clear result
std::string().swap(result1);
t1 = std::chrono::high_resolution_clock::now();
result1 = std::regex_replace (bigstring, std_rx, "$1\n$2", std::regex_constants::format_no_copy);
t2 = std::chrono::high_resolution_clock::now();
std_regex_duration = (std_regex_duration*i + (t2 - t1)) / (i + 1);
// clear result
std::string().swap(result2);
t1 = std::chrono::high_resolution_clock::now();
result2 = pcrscpp_rx.replace_copy (bigstring);
t2 = std::chrono::high_resolution_clock::now();
pcrscpp_duration = (pcrscpp_duration*i + (t2 - t1)) / (i + 1);
}
std::cout << "Time taken by std::regex_replace: "
<< std_regex_duration.count()
<< " ms" << std::endl
<< "Result size: " << result1.size() << std::endl;
std::cout << "Time taken by pcrscpp::replace: "
<< pcrscpp_duration.count()
<< " ms" << std::endl
<< "Result size: " << result2.size() << std::endl;
return 0;
}

(注意,stdpcrscpp正则表达式在这里做同样的事情,pcrscpp的表达式中尾随换行符是由于std::regex_replace不剥离换行符,尽管std::regex_constants::format_no_copy)

并在一个大的(20.9 MB) shell移动脚本上启动它:

Running tests 10 times: be patient...
Time taken by std::regex_replace: 12090771487 ms
Result size: 101087330
Time taken by pcrscpp::replace: 5910315642 ms
Result size: 101087330

可以看到,PCRSCPP比PCRSCPP快2倍以上。我预计这个差距会随着模式复杂性的增加而增加,因为PCRE可以更好地处理复杂的模式。我最初为自己写了一个包装器,但我认为它对其他人也很有用。

问候,亚历克斯

我不认为你做错了什么,比如说,c++正则表达式库只是没有python库那么快(至少在这个用例中是这样)。这并不太令人惊讶,记住python的正则表达式代码也都是C/c++代码,并且多年来一直被调得非常快,因为这是python中相当重要的特性,所以它自然会非常快。

但是如果需要的话,c++中还有其他的选项可以让事情变得更快。我过去使用过PCRE (http://pcre.org/),效果很好,不过我相信现在也有其他好的工具。

但是,对于这种情况,您也可以在没有正则表达式的情况下实现您所追求的,在我的快速测试中,这产生了10倍的性能改进。例如,下面的代码扫描输入字符串,将所有内容复制到一个新的缓冲区,当它遇到<时,它开始跳过字符,直到看到结束的>

std::string buffer(size, ' ');
std::string outbuffer(size, ' ');
... read in buffer from your file
size_t outbuffer_len = 0;
for (size_t i=0; i < buffer.size(); ++i) {
if (buffer[i] == '<') {
while (buffer[i] != '>' && i < buffer.size()) {
++i;
}
} else {
outbuffer[outbuffer_len] = buffer[i];
++outbuffer_len;
}
}
outbuffer.resize(outbuffer_len);