【问题标题】:std::regex: Ubuntu(15.10)-Clang++ produces much more performant binaries than Debian-8-Clang++ (both v.3.4.)std::regex: Ubuntu(15.10)-Clang 生成的二进制文件比 Debian-8-Clang (v.3.4.)
【发布时间】:2015-11-02 12:49:24
【问题描述】:

我创建了一个测试程序,在解析 csv 数据时测量 std::regex 的性能:

#include <string.h>
#include <iostream>
#include <stdexcept>
#include <chrono>
#include <regex>
#include <set>
#include <iomanip>

#define DEFAULT_REGEX                                 \
    R"(^((?:[^\x00-\x1F\x80-\xFF\\;]|\\\\|\\;)*);)"   \
    R"((L|P|D|DN|R|W|LS|PS|RS|LU|PU|RU|LK|PK|RK|F);)" \
    R"(((?:[^\x00-\x1F\x80-\xFF\\;]|\\\\|\\;)*);)"    \
    R"(((?:[^\x00-\x1F\x80-\xFF\\;]|\\\\|\\;)*);)"    \
    R"(((?:[^\x00-\x1F\x80-\xFF\\;]|\\\\|\\;|\\:)*))" \
    R"((?:;((?:[^\x00-\x1F\x80-\xFF\\;])"             \
    R"(|\\\\|\\;|\';\')*))?$)"

struct results_t {
    std::string address;
    std::string command;
    std::string client;
    std::string param;
    std::string value;
    std::string error;
};

void std_regex(std::size_t num, const std::string &str, results_t &res) {
    std::smatch pieces;
    static const std::regex pattern{DEFAULT_REGEX};
    for (auto i = 0u; i < num; i++) {
        bool matched = std::regex_match(str, pieces, pattern);
        if (!(matched && pieces.size() == 7)) {
            throw std::runtime_error("ERROR");
        }
    }
    res.address = pieces[1];
    res.command = pieces[2];
    res.client = pieces[3];
    res.param = pieces[4];
    res.value = pieces[5];
    res.error = pieces[6];
}

std::size_t get_median(const std::multiset<std::size_t> &measured_values) {
    std::size_t i = 0;
    std::size_t median = 0;
    for (auto it = measured_values.cbegin();; it++, i++) {
        double tmp = static_cast<double>(measured_values.size() - 1) / 2.0;
        if (i == floor(tmp)) {
            median = *it;
        }
        if (i == ceil(tmp)) {
            median += *it;
            break;
        }
    }
    return static_cast<std::size_t>(static_cast<double>(median) / 2.0 + 0.5);
}

std::size_t get_avg(const std::multiset<std::size_t> &measured_values) {
    return static_cast<std::size_t>(
        std::accumulate(measured_values.cbegin(), measured_values.cend(), 0) /
            static_cast<double>(measured_values.size()) +
        0.5);
}

int main(void) {
    constexpr std::size_t num = 100000;
    constexpr std::size_t measure_num = 250;
    std::string str = "zzz\\\\bbbb;L;babaa;bubu\\;cc;vvvv;asdff";

    std::multiset<std::size_t> measured_values;
    results_t res;

    for (std::size_t i = 0; i < measure_num; i++) {
        auto start = std::chrono::system_clock::now();
        std_regex(num, str, res);
        auto end = std::chrono::system_clock::now();
        measured_values.insert(
            std::chrono::duration_cast<std::chrono::microseconds>(end - start)
                .count());
    }

    std::cout << *measured_values.cbegin() << ";"           // min
              << *measured_values.crbegin() << ";"          /// max
              << get_avg(measured_values) << ";"            // average
              << get_median(measured_values) << std::endl;  // median
}

使用 Ubuntu 15.10 和 Debian 8,编译代码(没有错误或警告):

clang++-3.4 -DCOMPILER='"clang++-3.4"' -Wall -pedantic-errors -Werror -Wextra -DNDEBUG -O3 -mtune=native -march=native -std=c++1y -o eval_clang_3_4 eval.cpp

正如预期的那样,如果使用不同的编译器,这个程序会显示不同的时间。例如。如果你使用 g++5.2 而不是 g++4.9,性能会变得更好。

但是这个评估程序也显示了一个有趣的特性:如果你在 Debian 8 而不是 Ubuntu 15.10 上使用 clang++-3.4,它会产生更糟糕的时间。该软件在同一台机器上运行两次(Intel i7-3770k 和 8GB RAM),在这两种情况下,都使用了 clang++-3.4。

评估执行了 250 次,在以下几行中,您可以看到此测量的统计信息。

这是 Debian 8 上的测量值:(min;max;avg;median)

691244;1160628;713112;700739

以下是 Ubuntu 15.10 上的测量值:(min;max;avg;median)

198484;290986;202656;200637

我不关心这个,如果差异大约是 10% 或 20%,但在这种情况下,差异大约是 350%。

为什么在执行这个二进制文件时会有这么大的不同?

【问题讨论】:

  • 看起来error 的正则表达式是(具有讽刺意味的)错误的。它有......奇怪的括号。你真的是说(|\\|\;|';')*吗? (答案:没有)
  • 不正则表达式没有错,左边的括号包含到R"(
  • 我知道。你错过了我的意思。
  • 如果您有兴趣,我有一个更易于维护且速度更快的替代实现:stackoverflow-sehe.s3.amazonaws.com/…

标签: c++ regex parsing ubuntu debian


【解决方案1】:

我已经做了一些基准测试,详细说明了my earlier answer 中的测试。

我在

中创建了替代解析器实现
  • 灵气 (v2.x)
  • Spirit X3(仅限 c++14,实验性)
  • 手动解析器(写成 c++14 风格,但可以很容易地制作成 c++03)

性能结果:

很明显,无论使用何种编译器,手写解析器都是胜出者。

Spirit X3 紧随其后。

Spirit Qi完全匹配std_regex的性能,除了libc++,因为std_regex只是慢。

总结:

我建议使用 Spirit 或手动解析器,因为:

  • 正则表达式是维护的噩梦(实际上是I already spotted errors
  • 所有三个备选方案都会为您提供更有用的结果(因为转义序列是实际解释的,因此您不必再次处理这些)
  • X3 语法非常容易维护

备选方案 1:精神 X3

如果您负担得起使用需要 C++14 的实验性 boost 库,这是我个人的最爱。查看代码你会明白为什么:

void spiritX3(const std::string &payload, results_t &res) {

    using namespace boost::spirit::x3;
    auto escaping = [](auto&& set) { return ('\\' >> char_(set)) | (print - char_(set)); };
    auto text     = escaping(";\\");

    symbols<unused_type> cmds;
    cmds += "L", "P", "D", "DN", "R", "W", "LS", "PS", "RS", "LU", "PU", "RU", "LK", "PK", "RK", "F";

    auto address_ = *text;
    auto command_ = raw [ cmds ];
    auto client_  = *text;
    auto param_   = *text;
    auto value_   = *escaping(";:\\"); // note the ':'
    auto error_   = *("'" >> char_(';') >> "'" | escaping(";\\"));

    auto attr = std::tie(res.address, res.command, res.client, res.param, res.value, res.error);

    if (!parse(
            payload.begin(), payload.end(),
            address_ >> ';' >> command_ >> ';' >> client_ >> ';' >> param_ >> ';' >> value_ >> -(';' >> error_),
            attr)) 
    {
        throw std::runtime_error("ERROR");
    }
}

方案二:灵气

这基本上是 X3 语法的直接反映,但使用了一些宏滥用来弥补 Qi 的限制(您也可以通过更多重复代码来“修复”它)。

Spirit Qi 具有与 C++03 完全兼容的优势,并且是近十年来稳定提升的一部分:

void spiritQi(const std::string &payload, results_t &res) {

    using namespace boost::spirit::qi;

#define ESCAPING(set) (('\\' >> char_(set)) | (print - char_(set)))
#define TEXT *ESCAPING(";\\")

    symbols<char, unused_type> cmds;
    cmds += "L", "P", "D", "DN", "R", "W", "LS", "PS", "RS", "LU", "PU", "RU", "LK", "PK", "RK", "F";

    using It = std::string::const_iterator;
    rule<It, std::string()> address_ = TEXT;
    rule<It, std::string()> command_ = raw [ cmds ];
    rule<It, std::string()> client_  = TEXT;
    rule<It, std::string()> param_   = TEXT;
    rule<It, std::string()> value_   = *ESCAPING(";:\\"); // note the ':'
    rule<It, std::string()> error_   = *("'" >> char_(';') >> "'" | ESCAPING(";\\"));

    BOOST_SPIRIT_DEBUG_NODES((address_)(command_)(client_)(param_)(value_)(error_))

#undef TEXT
#undef ESCAPING

    auto attr = std::tie(res.address, res.command, res.client, res.param, res.value, res.error);

    if (!parse(
            payload.begin(), payload.end(),
            address_ >> ';' >> command_ >> ';' >> client_ >> ';' >> param_ >> ';' >> value_ >> -(';' >> error_),
            attr)) 
    {
        throw std::runtime_error("ERROR");
    }
}

备选方案 3:手动解析代码

这段代码没有任何依赖,是完全标准的 c++。

当然,如您所见,这将需要更多的编码。

我们使用 C++14 lambda 使其“自包含”,但在 C++03 中编写等效的解析代码很“容易”,优化后应该会产生相同的性能。

void manual(const std::string &payload, results_t &res) {

    using It = std::string::const_iterator;
    It       it  = payload.begin();
    It const end = payload.end();

    auto consume = [&](char const* escape_set, std::string& into, auto&& specials) {
        while (it != end)
            if (!specials(into)) switch (*it) {
                case '\\':
                    if (++it != end && strchr(escape_set, *it))
                        into += *it++;
                    else
                        throw "invalid escape";
                    break;
                default:
                    if (isprint(*it) && !strchr(escape_set, *it))
                        into += *it++;
                    else
                        return true;
            }
        return true;
    };

    auto escaping = [&](char const* escape_set, std::string& into) {
        return consume(escape_set, into, [](std::string&) { return false; });
    };
    auto matched = [&](char const* what) {
        auto saved = it;
        auto wit = what;
        while (*wit) {
            if (it != end && *wit == *it)
                { ++wit; ++it; }
            else {
                it = saved;
                // throw "expected: '" + std::string(what);
                return false;
            }
        }

        return true;
    };

    auto expect = [&](char const* what) {
        if (!matched(what))
            throw "expected: '" + std::string(what);
        return true;
    };

    auto cmd = [&](std::string& into) {
        static const char *const cmds[] = { "D", "DN", "F", "L", "LK", "LS", "LU", "P", "PK", "PS", "PU", "R", "RK", "RS", "RU", "W" };
        for (auto cmd : cmds)
            if (matched(cmd)) {
                into.assign(cmd);
                return true;
            }
        return false;
    };

    bool ok =  escaping(";\\", res.address) && expect(";")
            && cmd(res.command)                 && expect(";")
            && escaping(";\\",  res.client)     && expect(";")
            && escaping(";\\",  res.param)      && expect(";")
            && escaping(":;\\", res.value);

    auto squoted_semicolon = [&](std::string& into) {
        if (!matched("';'"))
            return false;
        into += ';';
        return true;
    };

    ok &= (it==end) || (expect(";") && consume(";\\", res.error, squoted_semicolon));

    if (!ok)
        throw std::runtime_error("ERROR");
}

样本输出

使用 libc++ 配置 clang 3.6 的输出:

---- parsed with regex:
address: zzz\\bbbb
command: L
client:  babaa
param:   bubu\;cc
value:   vvvv
error:   asd';'ff
---- parsed with manual parser (note: unescaping taken care of):
address: zzz\bbbb
command: L
client:  babaa
param:   bubu;cc
value:   vvvv
error:   asd;ff
---- parsed with spirit Qi (note: unescaping taken care of):
address: zzz\bbbb
command: L
client:  babaa
param:   bubu;cc
value:   vvvv
error:   asd;ff
clock resolution: mean is 16.9379 ns (40960002 iterations)

benchmarking std_regex
collecting 100 samples, 1 iterations each, in estimated 4.968 ms
mean: 15.2716 μs, lb 14.8763 μs, ub 16.1072 μs, ci 0.95
std dev: 2.81028 μs, lb 1668.21 ns, ub 5.63468 μs, ci 0.95
found 1 outliers among 100 samples (1%)
variance is severely inflated by outliers

benchmarking spirit Qi
collecting 100 samples, 7 iterations each, in estimated 1780.1 μs
mean: 2.15209 μs, lb 2.06754 μs, ub 2.22874 μs, ci 0.95
std dev: 412.372 ns, lb 369.921 ns, ub 453.462 ns, ci 0.95
found 0 outliers among 100 samples (0%)
variance is severely inflated by outliers

benchmarking manual
collecting 100 samples, 37 iterations each, in estimated 1705.7 μs
mean: 451.902 ns, lb 448.665 ns, ub 459.504 ns, ci 0.95
std dev: 23.7123 ns, lb 7.42683 ns, ub 41.7546 ns, ci 0.95
found 2 outliers among 100 samples (2%)
variance is severely inflated by outliers

【讨论】:

    【解决方案2】:

    基准测试看起来存在致命缺陷,因为您将样本存储在set,而不是multiset

    我将发布一个带有 Nonius(微基准框架)的固定版本,并展示 GCC 5 和 Clang 3.6 之间的区别

    简单对比:

    未使用:http://paste.ubuntu.com/13082145/(见下文)

    • GCC/libstdc++ 输出 - interactive graph

      clock resolution: mean is 16.4758 ns (40960002 iterations)
      
      benchmarking testcase
      collecting 100 samples, 1 iterations each, in estimated 2.3705 ms
      mean: 2.65143 μs, lb 2.56397 μs, ub 3.0819 μs, ci 0.95
      std dev: 856.395 ns, lb 13.87 ns, ub 2.04304 μs, ci 0.95
      found 4 outliers among 100 samples (4%)
      variance is severely inflated by outliers
      
    • Clang/libc++ 输出 interactive graph

      clock resolution: mean is 16.2365 ns (40960002 iterations)
      
      benchmarking testcase
      collecting 100 samples, 1 iterations each, in estimated 5.1507 ms
      mean: 14.5007 μs, lb 14.1065 μs, ub 15.6277 μs, ci 0.95
      std dev: 3.16854 μs, lb 1395.03 ns, ub 6.91175 μs, ci 0.95
      found 1 outliers among 100 samples (1%)
      variance is severely inflated by outliers
      
    • Clang/libstdc++ 输出 interactive graph

      clock resolution: mean is 16.7234 ns (40960002 iterations)
      
      benchmarking testcase
      collecting 100 samples, 1 iterations each, in estimated 2.468 ms
      mean: 3.48597 μs, lb 3.39304 μs, ub 3.92522 μs, ci 0.95
      std dev: 879.913 ns, lb 74.8209 ns, ub 2.09227 μs, ci 0.95
      found 8 outliers among 100 samples (8%)
      variance is severely inflated by outliers
      

    结论?

    很明显

    • 测量这个无噪声的微型基准测试很困难
    • libc++ 与 clang 结合起来似乎慢了大约 2 倍
    • 使用 clang/gcc 的影响似乎较小(尽管平均而言存在一些差异,但差异使得很难说它是相关的)

    代码清单

    #include <iostream>
    #include <nonius/benchmark.h++>
    #include <nonius/main.h++>
    #include <regex>
    
    #define DEFAULT_REGEX                                 \
        R"(^((?:[^\x00-\x1F\x80-\xFF\\;]|\\\\|\\;)*);)"   \
        R"((L|P|D|DN|R|W|LS|PS|RS|LU|PU|RU|LK|PK|RK|F);)" \
        R"(((?:[^\x00-\x1F\x80-\xFF\\;]|\\\\|\\;)*);)"    \
        R"(((?:[^\x00-\x1F\x80-\xFF\\;]|\\\\|\\;)*);)"    \
        R"(((?:[^\x00-\x1F\x80-\xFF\\;]|\\\\|\\;|\\:)*))" \
        R"((?:;((?:[^\x00-\x1F\x80-\xFF\\;])"             \
        R"(|\\\\|\\;|\';\')*))?$)"
    
    struct results_t {
        std::string address, command, client, param, value, error;
    };
    
    static const std::regex pattern{DEFAULT_REGEX};
    
    void std_regex(const std::string &payload, results_t &res) {
        std::smatch pieces;
        bool matched = std::regex_match(payload, pieces, pattern);
    
        if (!matched || pieces.size() != 7) {
            throw std::runtime_error("ERROR");
        }
    
        res = { pieces[1], pieces[2], pieces[3], pieces[4], pieces[5], pieces[6] };
    }
    
    static std::string const payload = "zzz\\\\bbbb;L;babaa;bubu\\;cc;vvvv;asdff";
    
    NONIUS_BENCHMARK("testcase", [](/*nonius::chronometer cm*/) {
        results_t res;
        std_regex(payload, res);
    })
    

    【讨论】:

    • 除了其他基准测试问题之外,您正在测试标准的 Iibrary 组件,而不是编译器。两台机器可能有不同的库版本。
    • 是的。它是针对 libstdc++ 的 libc++。虽然不需要不同的机器。请注意,是 OP 这样做的,所以也许您打算在这个问题上发表您的评论?
    • 哎呀,我已经纠正了我原来帖子中 std::set 的问题
    • 是的,使用 android 进行评论是不精确的 :-) 关键是仅仅将 g++ 更改为 clang++ 并不会改变您使用的库。
    • @byteunit 我已经更新了我的答案。基准测试有很多微妙之处,我衷心推荐(真棒)github.com/rmartinho/nonius
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-02-14
    • 2011-03-12
    • 2020-01-23
    • 1970-01-01
    • 2022-12-10
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多