[英]c++11 regex slower than python
嗨,我想了解为什么下面的代码使用正则表达式进行拆分字符串拆分
#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
另请参见以下答案: https : //stackoverflow.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()
不会进行内存释放,向量将维护内存并在原位构造字符串。
另一个性能提升将是完全避免构造/销毁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
作为返回值,其中每个char指针都指向原始s
c字符串本身内的一个子字符串 。 问题是,您不能这样做,因为它们每个都不会以null终止(为此,请参阅后面的示例中的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(从主干)构建了示例。 也许其他正则表达式库可以执行得更好,但是在任何情况下,分配/解除分配通常都会影响性能。
这是c字符串参数示例的boost::regex
计时:
real 0m1.284s
user 0m1.278s
sys 0m0.005s
此示例中的相同代码, boost::regex
和std::regex
接口是相同的,只需要更改名称空间和包含即可。
C ++ stdlib regex实现正处在起步阶段,它祝愿它随着时间的推移变得更好。
为了完善起见,我尝试了这一点(上面提到的“最终改进”建议),但在任何情况下,它都没有提高等效的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和string_ref建议有关 。 这是使用它的示例代码:
#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;
}
对于使用vector return进行split
的情况,返回string_ref
的向量而不是string
副本会更便宜。
这个新的解决方案能够通过返回获得输出。 我使用了在https://github.com/mclow/string_view中找到的Marshall Clow的string_view
(将string_ref
重命名)libc ++实现。
#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
(或以null结尾的string )上创建string_view
,因此它非常轻巧,不会产生不必要的字符串分配。
只是为了比较使用此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();
}
}
它使用升压范围复制算法在每次迭代中填充向量,时序为:
real 0m1.002s
user 0m0.997s
sys 0m0.004s
可以看出,与优化的string_view
输出参数版本相比,没有太大差异。
还请注意,有一个关于std::split
的建议可以这样工作。
通常,对于优化,您要避免两件事:
这两者可能是对立的,因为有时最终它比将所有内容都缓存在内存中来更快地进行计算……因此这是一种平衡游戏。
让我们分析一下您的代码:
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所说)。
这个版本怎么样? 它不是正则表达式,但是可以很快解决拆分问题。
#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;
}
$时间splittest.exe
真实的0m0.132s用户0m0.000s sys 0m0.109s
我会说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大约一秒钟的时间,而使用regex的C ++ 11程序则需要花费更多的时间(!)。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.