c++11 regex slower than python
嗨,我想知道为什么下面的代码使用regex来分割字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | #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代码慢
1 2 3 | import re for i in range(10000): re.split(' +', 'a b c') |
这里是
1 2 | > 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编译会使它降到
通知
另请参阅此答案:https://stackoverflow.com/a/21708215,它是底部编辑2的基础。
我已经将循环增加到了1000000,以获得更好的时间度量。
这是我的python计时:
1 2 3 | real 0m2.038s user 0m2.009s sys 0m0.024s |
这里有一个相当于您的代码,只是稍微漂亮一点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #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; } |
时机:
1 2 3 | real 0m5.786s user 0m5.779s sys 0m0.005s |
这是一种避免构造/分配向量和字符串对象的优化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | #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; } |
时机:
1 2 3 | real 0m3.034s user 0m3.029s sys 0m0.004s |
这几乎是100%的性能改进。
向量是在循环之前创建的,并且可以在第一次迭代中增长其内存。之后,
另一个性能提升将是完全避免构造/破坏
这是一个暂时的方向:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | #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; } } |
时机:
1 2 3 | real 0m2.509s user 0m2.503s sys 0m0.004s |
最终的改进是返回
最后的改进也可以通过以下方式实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | #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; } |
我用3.3 clang(主干)和-o3制作了样品。也许其他的regex库能够更好地执行,但是在任何情况下,分配/释放常常会影响性能。
增压器这是c字符串参数示例的
1 2 3 | real 0m1.284s user 0m1.278s sys 0m0.005s |
相同的代码,此示例中的
随着时间的推移,最好的愿望是,C++STDLIB ReGEX实现是在他们的幼年期。
编辑为了完成这项工作,我尝试过(上述"最终改进"建议),但没有在任何方面提高等效的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | #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; } |
这与数组引用和字符串引用建议有关。下面是使用它的示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | #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; } |
对于带有矢量返回的
这个新的解决方案能够通过返回获得输出。我使用了Marshall Clow的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | #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); } } |
时机:
1 2 3 | real 0m0.385s user 0m0.385s sys 0m0.000s |
请注意,与以前的结果相比,这一过程要快得多。当然,它不会填充循环中的
由于在
为了比较使用这个
1 2 3 4 5 6 7 8 9 | 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(); } } |
这将使用增强范围复制算法在每次迭代中填充向量,时间为:
1 2 3 | real 0m1.002s user 0m0.997s sys 0m0.004s |
可以看出,与优化的
注意,还有一个关于
对于优化,通常要避免两件事:
- 为不必要的东西烧掉CPU周期
- 空闲地等待发生的事情(内存读取、磁盘读取、网络读取…)
这两个可能是对立的,因为有时它最终会比将其全部缓存在内存中更快地计算出某些东西…所以这是一个平衡的游戏。
让我们分析您的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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]:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | // 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 |
在您执行的测试中,子匹配的数量在迭代过程中是恒定的,这个版本不太可能被击败:它只会在第一次运行时分配内存(对于
【1】:免责声明,我只是证明了这个代码是正确的,我没有测试它(如唐纳德·克努斯所说)。
这个威瑞恩怎么样?它不是regex,但它可以很快地解决分裂…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | #include <vector> #include <string> #include 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文件
实0.132s用户0.000秒系统0M0.109S
我认为C++ 11正则表达式比Perl慢,可能比Python慢得多。
为了正确测量性能,最好进行测试使用一些不平凡的表达式,或者你正在测量一切但是正则表达式本身。
例如,比较C++ 11和perl
C++ 11测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | #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; } |
在我用GCC4.9.3编译的计算机中,我得到了输出
1 | 10000/10000 matches 1704 ms --> 0.1704 ms per regex_search |
Perl测试码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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 "; |
在我的计算机中再次使用Perlv5.8.8
1 | 1000000/1000000 matches 2929.55303192139 ms --> 0.00292955303192139 ms per regex |
因此,在这个测试中,C++/Perl的比率为11。
1 | 0.1704 / 0.002929 = 58.17 times slower than perl |
在真实的场景中,我得到的比率大约慢100到200倍。因此,举例来说,分析一个包含一百万行的大文件需要Perl大约需要一秒钟,而它可能需要更多的时间!!)对于C++ 11使用regex编程。