Как повысить производительность анализатора ключа-значения boost::spirit::x3
Я анализирую пары ключ-значение (аналогично заголовкам HTTP), используя boost::spirit::x3
, При сравнении производительности с моим рукописным парсером, 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;
}
2 ответа
Вот фиксированная грамматика 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 мкс против 3.5 мкс в среднем.
Полный код
Использование 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
После первого просмотра пользовательского парсера мне приходит в голову, что он не такой надежный, как анализатор духа.
Если вы измените строку 91, чтобы удалить \r
из заднего "\r\n"
вы поймете, что я имею в виду.
Неверные данные могут привести к тому, что свернутый вручную файл перестанет работать.
например:
str.append("key" + num + ": " + "value" + num + "\n");
приводит к segfault на линии 46.
Итак, первый шаг, я думаю, состоит в том, чтобы модифицировать свернутый вручную парсер так, чтобы он проверял граничные условия так же, как и дух.
Хотя я не ожидаю, что время сойдет полностью, они будут ближе.