c ++ 11 regex比python慢


77

嗨,我想了解为什么下面的代码使用正则表达式进行拆分字符串拆分

#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


8
您正在运行调试版本吗?使用模板时,请确保已选择启用并调试;否则,很多安全检查最终会出现在您的代码中。
ssube

25
他们做的不一样。例如,C ++代码会进行字符串连接,而Python不会。
interjay

21
对于Python而言,正则表达式只能编译/优化一次。C ++正则表达式库将一次又一次地构建和优化正则表达式。仅作记录,请尝试将rsplit正则表达式定义为静态常量。对于Python,re库可以与编译器一起使用,并维护一系列优化的正则表达式。
圣地亚哥塞维利亚

8
这就是为什么人们将python用于此类任务的原因:它使程序员无需进行有关影响性能的非常技术性的分析。
Marcin 2013年

9
我可以大致重现您的结果,只需用boost :: regex替换libc ++的std :: regex即可使C ++版本击败python约10-15%。我认为libc ++的实现还不是特别有效。
Cubbi

Answers:


99

注意

另请参见以下答案: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

最终的改进是有一个std::vectorconst char *作为回报,其中每个字符指针会指向原来的内子s C字符串本身。问题是,您不能这样做,因为它们中的每一个都不会被null终止(为此,请参阅string_ref后面的示例中的C ++ 1y用法)。


最后的改进也可以通过以下方式实现:

#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

这是boost::regex用于定时C字符串参数样品:

real    0m1.284s
user    0m1.278s
sys     0m0.005s

此示例中相同的代码boost::regexstd::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;
}

对于带有矢量返回的情况,返回一个矢量string_ref而不是string副本也将更便宜split

编辑2

这个新的解决方案能够通过返回获得输出。我使用了在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_view了一个多原稿S string(或一个零终止的字符串),这变得非常轻便,从不产生不必要串分配。

只是为了比较使用此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可以像这样工作。


要尝试的另一件事:static const string s("a b c");split(s,r,v)
jthill 2013年

@jthill我猜想它将改善std :: string参数版本,但是我猜想静态是没有必要的,只需声明为循环外即可,而不是先前从c字符串构造/销毁。
pepper_chico

1
@RnMss不需要return std::move(some_vector)何时some_vector是xvalue。我建议您在SO上寻找此关键字。无需依赖RVO / NRVO。
pepper_chico 2014年

1
您忘了添加std::regex::optimize到正则表达式中。这将使正则表达式使用确定性FSA。
bit2shift

1
请在您的答案顶部添加一个摘要,现在这对TL; DR人员来说非常困难:)
Dima Tisnek '16

5

通常,对于优化,您要避免两件事:

  • 烧掉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

在您执行的测试中,子匹配的次数在迭代中是恒定的,此版本不太可能被击败:它将仅在第一次运行时分配内存(forrsplitresult),然后继续重用现有内存。

[1]:免责声明,我只是证明此代码是正确的,而我没有对其进行测试(如Donald Knuth所说)。


3
我已经做了几乎完全相同的实现,但是省略了它,因为它对本示例没有任何改善。具有与push_back版本相同的性能...
pepper_chico 2013年

此外,通过查看,不要忘记在匹配项小于初始向量大小的情况下调整向量大小...嗯,嗯,只需返回size_t,就不需要了。但我觉得使用起来有点麻烦……
pepper_chico

1
@chico:我同意这resize件事,但是缩小尺寸的问题是,您会导致尾部的std::string重新分配,这会导致重新分配。当然,string_ref替代方案也不会遭受这种麻烦。
Matthieu M.

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;
}

$时间splittest.exe

实际0m0.132s用户0m0.000s sys 0m0.109s


0

我想说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倍。因此,例如,解析一百万行的大文件需要花费pers一秒钟的时间,而使用regex的C ++ 11程序则需要花费更多的时间(!)。


1
我试过同一今日(2019)用gcc 8.2和Perl 5.16,并得到了每1.8微秒regex_search与C ++每1.5微秒regex用Perl。我的观点是,性能非常依赖于实现,并且似乎libstdc ++中正则表达式的实现已得到很大改善。当我切换到boost.regex时使用C ++可以得到0.5 µsregex_search。这就是C ++的强大功能-您不会自动获得性能,但是可以控制它。
丹尼尔·兰格
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.