关于c ++:c ++ 11正则表达式比python慢

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编译会使它降到0.09s user 0.00s system 99% cpu 0.109 total


通知

另请参阅此答案: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%的性能改进。

向量是在循环之前创建的,并且可以在第一次迭代中增长其内存。之后,clear()没有内存释放,向量维护内存并在适当的位置构造字符串。

另一个性能提升将是完全避免构造/破坏std::string,从而避免分配/解除分配其对象。

这是一个暂时的方向:

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

最终的改进是返回const char *std::vector,其中每个char指针指向原始sc字符串本身内的子字符串。问题是,您不能这样做,因为它们中的每一个都不会被null终止(为此,请参见后面的示例中使用C++ 1y EDCOX1 5)。

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

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字符串参数示例的boost::regex计时:

1
2
3
real    0m1.284s
user    0m1.278s
sys     0m0.005s

相同的代码,此示例中的boost::regexstd::regex接口是相同的,只需更改名称空间和include即可。

随着时间的推移,最好的愿望是,C++STDLIB ReGEX实现是在他们的幼年期。

编辑

为了完成这项工作,我尝试过(上述"最终改进"建议),但没有在任何方面提高等效的std::vector &v版本的性能:

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

对于带有矢量返回的split,返回string_ref的矢量也比返回string的矢量要便宜。

编辑2

这个新的解决方案能够通过返回获得输出。我使用了Marshall Clow的string_view(string_ref更名)libc++实现,可在https://github.com/mclow/string_视图中找到。

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

请注意,与以前的结果相比,这一过程要快得多。当然,它不会填充循环中的vector,也不会提前匹配任何内容,但无论如何,您都会得到一个范围,您可以使用基于范围的for,甚至使用它来填充vector

由于在iterator_range的范围内,在原始string的范围内(或以空结束的字符串)创建string_view,因此这变得非常轻,不会产生不必要的字符串分配。

为了比较使用这个split实现,但实际上填充了一个vector,我们可以这样做:

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

可以看出,与优化的string_view输出参数版本相比没有太大的差异。

注意,还有一个关于std::split的建议,可以这样工作。


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

  • 为不必要的东西烧掉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

在您执行的测试中,子匹配的数量在迭代过程中是恒定的,这个版本不太可能被击败:它只会在第一次运行时分配内存(对于rsplitresult),然后继续重用现有内存。

【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编程。