c++11正则表达式比python慢
Posted
技术标签:
【中文标题】c++11正则表达式比python慢【英文标题】:c++11 regex slower than python 【发布时间】:2012-12-21 17:19:57 【问题描述】:您好,我想了解为什么以下代码使用正则表达式拆分字符串
#include<regex>
#include<vector>
#include<string>
std::vector<std::string> split(const std::string &s)
static const std::regex rsplit(" +");
auto rit = std::sregex_token_iterator(s.begin(), s.end(), rsplit, -1);
auto rend = std::sregex_token_iterator();
auto res = std::vector<std::string>(rit, rend);
return res;
int main()
for(auto i=0; i< 10000; ++i)
split("a b c", " ");
return 0;
比下面的python代码慢
import re
for i in range(10000):
re.split(' +', 'a b c')
这里是
> python test.py 0.05s user 0.01s system 94% cpu 0.070 total
./test 0.26s user 0.00s system 99% cpu 0.296 total
我在 osx 上使用 clang++。
使用 -O3 编译会将其降至 0.09s user 0.00s system 99% cpu 0.109 total
【问题讨论】:
您正在运行调试版本吗?使用模板时,请确保您已启用和禁用调试;否则,您的代码中会有很多安全检查。 他们不做同样的事情。例如,C++ 代码会进行字符串连接,而 Python 则不会。 Python 的正则表达式可能只编译/优化一次。 C++ 正则表达式库将一次又一次地构建和优化正则表达式。只是为了记录,尝试将rsplit
正则表达式定义为静态常量。对于 Python,re 库可以与维护优化的正则表达式列表的编译器一起使用。
这就是为什么人们使用 python 来完成这样的任务:它使程序员不必对影响性能的因素进行这些非常技术性的分析。
我可以大致重现您的结果,只需将 libc++ 的 std::regex 替换为 boost::regex 即可使 C++ 版本击败 python 约 10-15%。我觉得 libc++ 的实现还不是特别高效。
【参考方案1】:
通知
另请参阅此答案:https://***.com/a/21708215 这是此处底部 EDIT 2 的基础。
我已将循环增加到 1000000 以获得更好的计时测量。
这是我的 Python 时间:
real 0m2.038s
user 0m2.009s
sys 0m0.024s
这是你的代码的等价物,只是更漂亮一点:
#include <regex>
#include <vector>
#include <string>
std::vector<std::string> split(const std::string &s, const std::regex &r)
return
std::sregex_token_iterator(s.begin(), s.end(), r, -1),
std::sregex_token_iterator()
;
int main()
const std::regex r(" +");
for(auto i=0; i < 1000000; ++i)
split("a b c", r);
return 0;
时间:
real 0m5.786s
user 0m5.779s
sys 0m0.005s
这是避免构造/分配向量和字符串对象的优化:
#include <regex>
#include <vector>
#include <string>
void split(const std::string &s, const std::regex &r, std::vector<std::string> &v)
auto rit = std::sregex_token_iterator(s.begin(), s.end(), r, -1);
auto rend = std::sregex_token_iterator();
v.clear();
while(rit != rend)
v.push_back(*rit);
++rit;
int main()
const std::regex r(" +");
std::vector<std::string> v;
for(auto i=0; i < 1000000; ++i)
split("a b c", r, v);
return 0;
时间:
real 0m3.034s
user 0m3.029s
sys 0m0.004s
这接近于 100% 的性能提升。
向量在循环之前创建,并且可以在第一次迭代中增加其内存。之后clear()
没有释放内存,vector 维护内存并就地构造字符串。
另一个性能提升是完全避免构造/销毁std::string
,从而避免分配/释放其对象。
这是一个试探性的方向:
#include <regex>
#include <vector>
#include <string>
void split(const char *s, const std::regex &r, std::vector<std::string> &v)
auto rit = std::cregex_token_iterator(s, s + std::strlen(s), r, -1);
auto rend = std::cregex_token_iterator();
v.clear();
while(rit != rend)
v.push_back(*rit);
++rit;
时间:
real 0m2.509s
user 0m2.503s
sys 0m0.004s
最终的改进是将const char *
的std::vector
作为返回值,其中每个字符指针将指向原始s
c 字符串 本身内的子字符串。问题是,您不能这样做,因为它们中的每一个都不会以空值终止(为此,请参阅后面的示例中 C++1y string_ref
的用法)。
最后的改进也可以通过以下方式实现:
#include <regex>
#include <vector>
#include <string>
void split(const std::string &s, const std::regex &r, std::vector<std::string> &v)
auto rit = std::cregex_token_iterator(s.data(), s.data() + s.length(), r, -1);
auto rend = std::cregex_token_iterator();
v.clear();
while(rit != rend)
v.push_back(*rit);
++rit;
int main()
const std::regex r(" +");
std::vector<std::string> v;
for(auto i=0; i < 1000000; ++i)
split("a b c", r, v); // the constant string("a b c") should be optimized
// by the compiler. I got the same performance as
// if it was an object outside the loop
return 0;
我已经使用 -O3 使用 clang 3.3(来自主干)构建了示例。也许其他正则表达式库能够表现更好,但无论如何,分配/解除分配经常会影响性能。
Boost.Regex
这是 c 字符串 参数示例的boost::regex
计时:
real 0m1.284s
user 0m1.278s
sys 0m0.005s
代码相同,本示例中boost::regex
和std::regex
接口完全相同,只需要更改命名空间并包含即可。
祝愿它随着时间的推移变得更好,C++ 标准库正则表达式实现还处于起步阶段。
编辑
为了完成,我已经尝试过这个(上面提到的“终极改进”建议),但它并没有提高等效 std::vector<std::string> &v
版本的性能:
#include <regex>
#include <vector>
#include <string>
template<typename Iterator> class intrusive_substring
private:
Iterator begin_, end_;
public:
intrusive_substring(Iterator begin, Iterator end) : begin_(begin), end_(end)
Iterator begin() return begin_;
Iterator end() return end_;
;
using intrusive_char_substring = intrusive_substring<const char *>;
void split(const std::string &s, const std::regex &r, std::vector<intrusive_char_substring> &v)
auto rit = std::cregex_token_iterator(s.data(), s.data() + s.length(), r, -1);
auto rend = std::cregex_token_iterator();
v.clear(); // This can potentially be optimized away by the compiler because
// the intrusive_char_substring destructor does nothing, so
// resetting the internal size is the only thing to be done.
// Formerly allocated memory is maintained.
while(rit != rend)
v.emplace_back(rit->first, rit->second);
++rit;
int main()
const std::regex r(" +");
std::vector<intrusive_char_substring> v;
for(auto i=0; i < 1000000; ++i)
split("a b c", r, v);
return 0;
这与array_ref and string_ref proposal 有关。这是一个使用它的示例代码:
#include <regex>
#include <vector>
#include <string>
#include <string_ref>
void split(const std::string &s, const std::regex &r, std::vector<std::string_ref> &v)
auto rit = std::cregex_token_iterator(s.data(), s.data() + s.length(), r, -1);
auto rend = std::cregex_token_iterator();
v.clear();
while(rit != rend)
v.emplace_back(rit->first, rit->length());
++rit;
int main()
const std::regex r(" +");
std::vector<std::string_ref> v;
for(auto i=0; i < 1000000; ++i)
split("a b c", r, v);
return 0;
对于带有向量返回的split
的情况,返回string_ref
的向量而不是string
副本也会更便宜。
编辑 2
这个新的解决方案能够通过返回获得输出。我使用了 Marshall Clow 的 string_view
(string_ref
已重命名)libc++ 实现,位于 https://github.com/mclow/string_view。
#include <string>
#include <string_view>
#include <boost/regex.hpp>
#include <boost/range/iterator_range.hpp>
#include <boost/iterator/transform_iterator.hpp>
using namespace std;
using namespace std::experimental;
using namespace boost;
string_view stringfier(const cregex_token_iterator::value_type &match)
return match.first, static_cast<size_t>(match.length());
using string_view_iterator =
transform_iterator<decltype(&stringfier), cregex_token_iterator>;
iterator_range<string_view_iterator> split(string_view s, const regex &r)
return
string_view_iterator(
cregex_token_iterator(s.begin(), s.end(), r, -1),
stringfier
),
string_view_iterator()
;
int main()
const regex r(" +");
for (size_t i = 0; i < 1000000; ++i)
split("a b c", r);
时间:
real 0m0.385s
user 0m0.385s
sys 0m0.000s
请注意,与之前的结果相比,这有多快。当然,它不会在循环内填充vector
(也可能不会提前匹配任何内容),但无论如何你都会得到一个范围,你可以使用基于范围的for
来覆盖它,甚至可以用它来填充一个vector
.
由于iterator_range
的范围在原始string
(或以空终止的字符串)之上创建string_view
s,因此它变得非常轻量级,不会产生不必要的字符串分配。
只是为了比较使用这个split
实现,但实际上填充一个vector
,我们可以这样做:
int main()
const regex r(" +");
vector<string_view> v;
v.reserve(10);
for (size_t i = 0; i < 1000000; ++i)
copy(split("a b c", r), back_inserter(v));
v.clear();
这里使用boost range copy算法在每次迭代中填充向量,时间为:
real 0m1.002s
user 0m0.997s
sys 0m0.004s
可以看出,与优化后的string_view
输出参数版本相比没有太大区别。
还要注意a proposal for a std::split
可以像这样工作。
【讨论】:
还有一件事要尝试:static const string s("a b c");
和 split(s,r,v)
。
@jthill 我想它会改进 std::string 参数版本,但我想 static 不是必需的,只是被声明为循环之外就可以了,而不是之前的 c 构造/破坏字符串。
@RnMss 当some_vector
是一个xvalue 时,不需要return std::move(some_vector)
。我建议你在 SO 上寻找这个关键字。不依赖 RVO/NRVO。
您忘记将std::regex::optimize
添加到正则表达式。这将使正则表达式使用确定性 FSA。
请在您的答案顶部添加一个摘要,现在在 TL 上很难;DR 伙计们 :)【参考方案2】:
对于优化,一般来说,您要避免两件事:
为不必要的东西消耗 CPU 周期 等待某事发生(内存读取、磁盘读取、网络读取……)这两者可能是对立的,因为有时它最终会比将所有内容缓存在内存中更快地计算某些东西......所以这是一个平衡游戏。
让我们分析一下你的代码:
std::vector<std::string> split(const std::string &s)
static const std::regex rsplit(" +"); // only computed once
// search for first occurrence of rsplit
auto rit = std::sregex_token_iterator(s.begin(), s.end(), rsplit, -1);
auto rend = std::sregex_token_iterator();
// simultaneously:
// - parses "s" from the second to the past the last occurrence
// - allocates one `std::string` for each match... at least! (there may be a copy)
// - allocates space in the `std::vector`, possibly multiple times
auto res = std::vector<std::string>(rit, rend);
return res;
我们可以做得更好吗?好吧,如果我们可以重用现有存储而不是继续分配和释放内存,我们应该会看到显着的改进 [1]:
// Overwrites 'result' with the matches, returns the number of matches
// (note: 'result' is never shrunk, but may be grown as necessary)
size_t split(std::string const& s, std::vector<std::string>& result)
static const std::regex rsplit(" +"); // only computed once
auto rit = std::cregex_token_iterator(s.begin(), s.end(), rsplit, -1);
auto rend = std::cregex_token_iterator();
size_t pos = 0;
// As long as possible, reuse the existing strings (in place)
for (size_t max = result.size();
rit != rend && pos != max;
++rit, ++pos)
result[pos].assign(rit->first, rit->second);
// When more matches than existing strings, extend capacity
for (; rit != rend; ++rit, ++pos)
result.emplace_back(rit->first, rit->second);
return pos;
// split
在您执行的测试中,子匹配的数量在迭代中是恒定的,这个版本不太可能被击败:它只会在第一次运行时分配内存(rsplit
和 result
)然后继续重用现有内存。
[1]:免责声明,我只是证明了这段代码是正确的,我没有测试过它(正如 Donald Knuth 所说)。
【讨论】:
我做了一个几乎完全相同的实现,但省略了它,因为它没有改进这个示例的任何东西。获得与 push_back 版本相同的性能... 另外,通过查看,不要忘记调整向量的大小,以适应少于初始向量大小的匹配项......嗯,好吧,返回 size_t,这不是必需的。但是我觉得用起来有点麻烦…… @chico:我同意resize
的说法,但是缩小尺寸的问题是你会导致尾部std::string
的重新分配,这将导致重新分配。当然,string_ref
替代方案不会遭受这样的困扰。【参考方案3】:
这个版本怎么样?它不是正则表达式,但它可以非常快地解决拆分...
#include <vector>
#include <string>
#include <algorithm>
size_t split2(const std::string& s, std::vector<std::string>& result)
size_t count = 0;
result.clear();
std::string::const_iterator p1 = s.cbegin();
std::string::const_iterator p2 = p1;
bool run = true;
do
p2 = std::find(p1, s.cend(), ' ');
result.push_back(std::string(p1, p2));
++count;
if (p2 != s.cend())
p1 = std::find_if(p2, s.cend(), [](char c) -> bool
return c != ' ';
);
else run = false;
while (run);
return count;
int main()
std::vector<std::string> v;
std::string s = "a b c";
for (auto i = 0; i < 100000; ++i)
split2(s, v);
return 0;
$ time splittest.exe
真正的 0m0.132s 用户 0m0.000s 系统 0m0.109s
【讨论】:
【参考方案4】:我会说 C++11 正则表达式比 perl 慢得多,也可能比 python 慢。
要正确衡量性能,最好进行测试 使用一些不平凡的表达,否则你正在衡量一切 但正则表达式本身。
例如比较C++11和perl
C++11 测试代码
#include <iostream>
#include <string>
#include <regex>
#include <chrono>
int main ()
int veces = 10000;
int count = 0;
std::regex expres ("([^-]*)-([^-]*)-(\\d\\d\\d:\\d\\d)---(.*)");
std::string text ("some-random-text-453:10--- etc etc blah");
std::smatch macha;
auto start = std::chrono::system_clock::now();
for (int ii = 0; ii < veces; ii ++)
count += std::regex_search (text, macha, expres);
auto milli = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now() - start).count();
std::cout << count << "/" << veces << " matches " << milli << " ms --> " << (milli / (float) veces) << " ms per regex_search" << std::endl;
return 0;
在我使用 gcc 4.9.3 编译的计算机中,我得到了输出
10000/10000 matches 1704 ms --> 0.1704 ms per regex_search
perl 测试代码
use Time::HiRes qw/ time sleep /;
my $veces = 1000000;
my $conta = 0;
my $phrase = "some-random-text-453:10--- etc etc blah";
my $start = time;
for (my $ii = 0; $ii < $veces; $ii++)
if ($phrase =~ m/([^-]*)-([^-]*)-(\d\d\d:\d\d)---(.*)/)
$conta = $conta + 1;
my $elapsed = (time - $start) * 1000.0;
print $conta . "/" . $veces . " matches " . $elapsed . " ms --> " . ($elapsed / $veces) . " ms per regex\n";
在我的电脑上再次使用 perl v5.8.8
1000000/1000000 matches 2929.55303192139 ms --> 0.00292955303192139 ms per regex
所以在这个测试中 C++11 / perl 的比率是
0.1704 / 0.002929 = 58.17 times slower than perl
在实际场景中,我得到的比率会慢 100 到 200 倍。 因此,例如解析具有一百万行的大文件需要 perl 大约需要一秒钟,而 C++11 可能需要更多分钟(!) 使用正则表达式的程序。
【讨论】:
我今天(2019 年)使用 gcc 8.2 和 perl 5.16 进行了相同的尝试,并且使用 C++ 获得了 每regex_search
1.8 µs 和 每 regex
1.5 µs用 perl。我的观点是,性能非常依赖于实现,并且看起来,libstdc++ 中的正则表达式的实现有了很大的改进。当我切换到 boost.regex 时,使用 C++ 每regex_search
得到 0.5 µs。这就是 C++ 的力量——你不会自动获得性能,但你可以控制它。以上是关于c++11正则表达式比python慢的主要内容,如果未能解决你的问题,请参考以下文章
[C/C++11]_[初级]_[使用正则表达式库regex]
使用 python 和 javascript 的慢正则表达式,但在 go 和 php 中快速失败
python 正则表达式 re,compile速度慢 ,怎样可以使的re.compile的速度更快