【问题标题】:how to improve performance of boost::spirit::x3 key-value parser如何提高 boost::spirit::x3 键值解析器的性能
【发布时间】:2016-08-29 13:09:00
【问题描述】:

我正在使用 boost::spirit::x3 解析键值对(类似于 HTTP 标头)。将性能与我的手写解析器进行比较时,boost::spirit::x3 比这慢了大约 10%。

我正在使用 boost 1.61 和 GCC 6.1:

$ g++ -std=c++14 -O3 -I/tmp/boost_1_61_0/boost/ main.cpp  && ./a.out

phrase_parse 1.97432 microseconds
parseHeader 1.75742 microseconds

如何提高基于 boost::spirit::x3 的解析器的性能?

#include <iostream>
#include <string>
#include <map>
#include <chrono>

#include <boost/spirit/home/x3.hpp>
#include <boost/fusion/adapted/std_pair.hpp>

using header_map = std::map<std::string, std::string>; 

namespace parser
{
    namespace x3 = boost::spirit::x3;
    using x3::char_;
    using x3::lexeme;

    x3::rule<class map, header_map> const map = "msg";

    const auto key     = +char_("0-9a-zA-Z-");
    const auto value   = +~char_("\r\n");

    const auto header =(key >> ':' >> value >> lexeme["\r\n"]);
    const auto map_def = *header >> lexeme["\r\n"];

    BOOST_SPIRIT_DEFINE(map);
}


template <typename It>
void parseHeader(It& iter, It end, header_map& map)
{
    std::string key;
    std::string value;

    It last = iter;
    bool inKey = true;
    while(iter+1 != end)
    {
        if(inKey && *(iter+1)==':')
        {
            key.assign(last, iter+1);
            iter+=3;
            last = iter;
            inKey = false;
        }
        else if (!inKey && *(iter+1)=='\r' && *(iter+2)=='\n')
        {
            value.assign(last, iter+1);
            map.insert({std::move(key), std::move(value)});
            iter+=3;
            last = iter;
            inKey = true;
        }
        else if (inKey && *(iter)=='\r' && *(iter+1)=='\n') 
        {
            iter+=2;
            break;
        }
        else
        {
            ++iter;
        }
    }
}

template<typename F, typename ...Args>
double benchmark(F func, Args&&... args)
{
    auto start = std::chrono::system_clock::now();

    constexpr auto num = 10 * 1000 * 1000;
    for (std::size_t i = 0; i < num; ++i)
    {
        func(std::forward<Args>(args)...);
    }

    auto end = std::chrono::system_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);

    return duration.count() / (double)num;
}

int main()
{
    const std::size_t headerCount = 20;

    std::string str;
    for(std::size_t i = 0; i < headerCount; ++i)
    {
        std::string num = std::to_string(i);
        str.append("key" + num + ": " + "value" + num + "\r\n");
    }
    str.append("\r\n");

    double t1 = benchmark([&str]() {
        auto iter = str.cbegin();
        auto end = str.cend();

        header_map header;
        phrase_parse(iter, end, parser::map, boost::spirit::x3::ascii::blank, header);
        return header;
    });
    std::cout << "phrase_parse " << t1 << " microseconds"<< std::endl;

    double t2 = benchmark([&str]() {
        auto iter = str.cbegin();
        auto end = str.cend();

        header_map header;
        parseHeader(iter, end, header);
        return header;
    });
    std::cout << "parseHeader " << t2 << " microseconds"<< std::endl;
    return 0;
}

live example

【问题讨论】:

    标签: c++ boost c++14 boost-spirit-x3


    【解决方案1】:

    这是一个固定的 x3 语法,它更接近于您的手卷“解析器”:

    const auto key     = +~char_(':');
    const auto value   = *(char_ - "\r\n");
    
    const auto header = key >> ':' >> value >> "\r\n";
    const auto map    = *header >> "\r\n";
    

    当然,它仍然更严格、更健壮。另外,不要使用空格跳过器调用它,因为您的手动解析器也不会这样做。

    这是我的盒子的性能测量结果:

    平均 2.5µs 与 3.5µs 的统计数据。

    完整代码

    使用http://nonius.io 进行可靠的基准测试:

    #include <iostream>
    #include <string>
    #include <map>
    #include <nonius/benchmark.h++>
    
    #include <boost/spirit/home/x3.hpp>
    #include <boost/fusion/adapted/std_pair.hpp>
    
    using header_map = std::map<std::string, std::string>; 
    
    namespace parser
    {
        namespace x3 = boost::spirit::x3;
        using x3::char_;
    
        const auto key     = +~char_(':');
        const auto value   = *(char_ - "\r\n");
    
        const auto header = key >> ':' >> value >> "\r\n";
        const auto map    = *header >> "\r\n";
    }
    
    
    template <typename It>
    void parseHeader(It& iter, It end, header_map& map)
    {
        std::string key;
        std::string value;
    
        It last = iter;
        bool inKey = true;
        while(iter+1 != end)
        {
            if(inKey && *(iter+1)==':')
            {
                key.assign(last, iter+1);
                iter+=3;
                last = iter;
                inKey = false;
            }
            else if (!inKey && *(iter+1)=='\r' && *(iter+2)=='\n')
            {
                value.assign(last, iter+1);
                map.insert({std::move(key), std::move(value)});
                iter+=3;
                last = iter;
                inKey = true;
            }
            else if (inKey && *(iter)=='\r' && *(iter+1)=='\n') 
            {
                iter+=2;
                break;
            }
            else
            {
                ++iter;
            }
        }
    }
    
    static auto const str = [] {
        std::string tmp;
        const std::size_t headerCount = 20;
        for(std::size_t i = 0; i < headerCount; ++i)
        {
            std::string num = std::to_string(i);
            tmp.append("key" + num + ": " + "value" + num + "\r\n");
        }
        tmp.append("\r\n");
        return tmp;
    }();
    
    NONIUS_BENCHMARK("manual", [](nonius::chronometer cm) {
    
        cm.measure([]() {
            auto iter = str.cbegin();
            auto end = str.cend();
    
            header_map header;
            parseHeader(iter, end, header);
            assert(header.size() == 20);
            return header.size();
        });
    })
    
    NONIUS_BENCHMARK("x3", [](nonius::chronometer cm) {
    
        cm.measure([] {
            auto iter = str.cbegin();
            auto end = str.cend();
    
            header_map header;
            parse(iter, end, parser::map, header);
            assert(header.size() == 20);
            return header.size();
        });
    })
    
    #include <nonius/main.h++>
    

    我正在使用 gcc 5.4 和 Boost 1.61

    【讨论】:

    • 您的机器上是否也有原始语法的测量值?
    • @m.s.注意that“随机”从键/值内部去除空格:3.6µs。图:i.imgur.com/undefined.jpg
    • 无论如何,我认为你应该首先分析瓶颈。一定是分配。如果可能的话,我建议解析到 string_ref/string_view 并且可能保留 flat_map:brings the manual version under 2µs 使用此代码:paste.ubuntu.com/23108281
    • 传递给phrase_parse的船长
    • @m.s. :您可以使用raw 指令,它为您提供boost::iterator_range,而后者又可以轻松转换为string_view。
    【解决方案2】:

    在第一次查看自定义解析器后,我发现它不像精神解析器那么健壮。

    如果您更改第 91 行以从尾随 "\r\n" 中删除 \r,您就会明白我的意思了。

    错误的数据会导致手卷出现段错误。

    例如:

    str.append("key" + num + ": " + "value" + num + "\n");
    

    导致第 46 行出现段错误。

    因此,我认为第一步是修改手动解析器,使其以与精神解析器相同的方式检查边界条件。

    虽然我预计时间不会完全收敛,但它们会更接近。

    【讨论】:

    • 你是对的,手写解析器不如精神解析器强大,例如也没有验证密钥字符串 ("0-9a-zA-Z-");但是,基于spirit::x3 的可能仍有改进的可能,例如一个不同的船长等。这种建议是我正在寻找的
    • @m.s.您可以尝试简单的parse() 并明确跳过空格吗?但是,根据我的经验(以及我收到的建议),它很少会更快。 sprit 使用令人眼花缭乱的快速标准库作为它的默认空白跳过器。
    • @m.s.但第一个问题(重申)是你没有比较喜欢和喜欢。所以你没有关于精神版本可以或应该有多快的数据,因为没有可比较的手写基线。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-05-25
    • 1970-01-01
    相关资源
    最近更新 更多