Qi符号性能下降?

时间:2018-01-12 22:54:55

标签: c++ performance boost boost-spirit boost-spirit-qi

我想提出一个刚刚把我送到兔子洞的主题并提出一个问题 齐::符号。

这一切都是在我查看新的野兽库并阅读a tutorial example

时开始的

它从一个从http路径猜测mime类型的函数开始 扩展。我开始仔细观察并看到了这一点:

 auto const ext = [&path]
    {
        auto const pos = path.rfind(".");
        if(pos == boost::beast::string_view::npos)
            return boost::beast::string_view{};
        return path.substr(pos);
    }();

我花了一段时间才发现它是C ++风格的IIFE,用于初始化ext,同时声明它是常量。

无论如何,我开始测试是否会产生任何性能差异 这将证明可怕的可读性与直接实施相比。

这样做我开始想知道这是不会更好的实现 齐::符号。所以我想出了两个替代实现:

#include <boost/smart_ptr/scoped_array.hpp>
#include <boost/accumulators/accumulators.hpp>
#include <boost/accumulators/statistics/stats.hpp>
#include <boost/accumulators/statistics/mean.hpp>
#include <boost/accumulators/statistics/moment.hpp>
#include <boost/chrono.hpp>
#include <boost/spirit/include/qi.hpp>
#include <boost/spirit/include/qi_parse.hpp>
#include <boost/spirit/include/phoenix.hpp>
#include <boost/fusion/include/vector.hpp>
#include <boost/spirit/include/karma.hpp>
#include <boost/algorithm/string.hpp>
#include <boost/assign.hpp>

#include <iostream>
#include <string>
#include <vector>
#include <random>

using namespace boost::accumulators;
typedef boost::chrono::duration<long long, boost::micro> microseconds;
namespace qi = boost::spirit::qi;
namespace karma = boost::spirit::karma;
namespace ascii = qi::ascii;
namespace phx = boost::phoenix;

const std::map<const std::string, const std::string> mime_exts = {
    { ".htm",  "text/html" },
    { ".html", "text/html" },
    { ".php",  "text/html" },
    { ".css",  "text/css"  },
    { ".js",   "application/javascript" },
    { ".json", "application/json" },
    { ".xml",  "application/xml" },
    { ".swf",  "application/x-shockwave-flash" },
    { ".flv",  "video/x-flv" },
    { ".png",  "image/png" },
    { ".jpe",  "image/jpeg" },
    { ".jpeg", "image/jpeg" },
    { ".jpg",  "image/jpeg" },
    { ".gif",  "image/gif" },
    { ".bmp",  "image/bmp" },
    { ".ico",  "image/vnd.microsoft.icon" },
    { ".tif",  "image/tiff" },
    { ".tiff", "image/tiff" },
    { ".svg",  "image/svg+xml"},
    { ".svgz", "image/svg+xml"}
};


const char *mime_literals[] = {
    "text/html",
    "text/css",
    "text/plain",
    "application/javascript",
    "application/json",
    "application/xml",
    "application/x-shockwave-flash",
    "video/x-flv",
    "image/png",
    "image/jpeg",
    "image/gif",
    "image/bmp",
    "image/vnd.microsoft.icon",
    "image/tiff",
    "image/svg+xml"
};

template <typename Iterator>
struct mimetype_matching_parser : qi::grammar<Iterator, unsigned int()> {

    mimetype_matching_parser() : mimetype_matching_parser::base_type(m_start, "mimetype_matching_parser") {

        m_mime_extensions.add
            (".htm", 0)
            (".html", 0)
            (".php", 0)
            (".css", 1)
            (".txt", 2)
            (".js", 3)
            (".json", 4)
            (".xml", 5)
            (".swf", 6)
            (".flv", 7)
            (".png", 8)
            (".jpe", 9)
            (".jpeg", 9)
            (".jpg", 9)
            (".gif", 10)
            (".bmp", 11)
            (".ico", 12)
            (".tiff", 13)
            (".tif", 13)
            (".svg", 14)
            (".svgz", 14)
            ;

        using qi::no_case;

        m_start %= no_case[m_mime_extensions] >> qi::eoi;
    }

    qi::symbols<char, unsigned int>   m_mime_extensions;
    qi::rule<Iterator, unsigned int()> m_start;
};

std::string mime_extension(const std::string &n_path) {

    // First locate the extension itself
    const std::size_t last_dot = n_path.rfind(".");
    if (last_dot == std::string::npos) {
        return "application/text";
    }

    // and now pipe the extension into a qi symbols parser.
    // I don't know if this is any faster than a more trivial algorithm::ends_with
    // approach but I guess it won't be any slower
    const mimetype_matching_parser<std::string::const_iterator> p;
    unsigned int                        result;
    std::string::const_iterator         begin = n_path.begin() + last_dot;
    const std::string::const_iterator   end = n_path.end();

    try {
        if (qi::parse(begin, end, p, result) && (begin == end)) {
            return mime_literals[result];
        } else {
            return "application/text";
        }
    } catch (const std::exception &) {  // asio throws on invalid parse
        return "application/text";
    }
}

std::string mime_extension2(const std::string &n_path) {

    using boost::algorithm::iequals;

    auto const ext = [&n_path] {
        auto const pos = n_path.rfind(".");
        if (pos == std::string::npos)
            return std::string{};
        return n_path.substr(pos);
    }();

    //  const std::size_t pos = n_path.rfind(".");
    //  if (pos == std::string::npos) {
    //      return std::string{};
    //  }
    //  const std::string ext = n_path.substr(pos);

    if (iequals(ext, ".htm"))  return "text/html";
    if (iequals(ext, ".html")) return "text/html";
    if (iequals(ext, ".php"))  return "text/html";
    if (iequals(ext, ".css"))  return "text/css";
    if (iequals(ext, ".txt"))  return "text/plain";
    if (iequals(ext, ".js"))   return "application/javascript";
    if (iequals(ext, ".json")) return "application/json";
    if (iequals(ext, ".xml"))  return "application/xml";
    if (iequals(ext, ".swf"))  return "application/x-shockwave-flash";
    if (iequals(ext, ".flv"))  return "video/x-flv";
    if (iequals(ext, ".png"))  return "image/png";
    if (iequals(ext, ".jpe"))  return "image/jpeg";
    if (iequals(ext, ".jpeg")) return "image/jpeg";
    if (iequals(ext, ".jpg"))  return "image/jpeg";
    if (iequals(ext, ".gif"))  return "image/gif";
    if (iequals(ext, ".bmp"))  return "image/bmp";
    if (iequals(ext, ".ico"))  return "image/vnd.microsoft.icon";
    if (iequals(ext, ".tiff")) return "image/tiff";
    if (iequals(ext, ".tif"))  return "image/tiff";
    if (iequals(ext, ".svg"))  return "image/svg+xml";
    if (iequals(ext, ".svgz")) return "image/svg+xml";
    return "application/text";
}

std::string mime_extension3(const std::string &n_path) {

    using boost::algorithm::iequals;

    auto ext = [&n_path] {
        auto const pos = n_path.rfind(".");
        if (pos == std::string::npos) {
            return std::string{};
        } else {
            return n_path.substr(pos);
        }
    }();

    boost::algorithm::to_lower(ext);

    const std::map<const std::string, const std::string>::const_iterator i = mime_exts.find(ext);

    if (i != mime_exts.cend()) {
        return i->second;
    } else {
        return "application/text";
    }
}

const std::string samples[] = {
    "test.txt",
    "test.html",
    "longer/test.tiff",
    "www.webSite.de/ico.ico",
    "www.websIte.de/longEr/path/ico.bmp",
    "www.TEST.com/longer/path/ico.svg",
    "googlecom/shoRT/path/index.HTM",
    "googlecom/bild.jpg",
    "WWW.FLASH.COM/app.swf",
    "WWW.FLASH.COM/BILD.GIF"
};

int test_qi_impl() {

    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<> dis(0, 10);

    const std::string sample = samples[dis(gen)];
    const std::string result = mime_extension(sample);

    int ret = dis(gen);
    for (const char &c : result) { ret += c; }
    return ret;
}

int test_lambda_impl() {

    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<> dis(0, 10);

    const std::string sample = samples[dis(gen)];
    const std::string result = mime_extension2(sample);

    int ret = dis(gen);
    for (const char &c : result) { ret += c; }
    return ret;
}

int test_map_impl() {

    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<> dis(0, 10);

    const std::string sample = samples[dis(gen)];
    const std::string result = mime_extension3(sample);

    int ret = dis(gen);
    for (const char &c : result) { ret += c; }
    return ret;
}

int main(int argc, char **argv) {

    const unsigned int loops = 100000;

    accumulator_set<boost::chrono::high_resolution_clock::duration, features<tag::mean> > times_qi;
    accumulator_set<boost::chrono::high_resolution_clock::duration, features<tag::mean> > times_lambda;
    accumulator_set<boost::chrono::high_resolution_clock::duration, features<tag::mean> > times_map;

    std::cout << "Measure execution times for " << loops << " lambda runs" << std::endl;
    for (unsigned int i = 0; i < loops; i++) {
        boost::chrono::high_resolution_clock::time_point start = boost::chrono::high_resolution_clock::now();
        test_lambda_impl();
        boost::chrono::high_resolution_clock::time_point end = boost::chrono::high_resolution_clock::now();
        times_lambda(end - start);
    }

    std::cout << "Measure execution times for " << loops << " qi runs" << std::endl;
    for (unsigned int i = 0; i < loops; i++) {
        boost::chrono::high_resolution_clock::time_point start = boost::chrono::high_resolution_clock::now();
        test_qi_impl();
        boost::chrono::high_resolution_clock::time_point end = boost::chrono::high_resolution_clock::now();
        times_qi(end - start);
    }

    std::cout << "Measure execution times for " << loops << " map runs" << std::endl;
    for (unsigned int i = 0; i < loops; i++) {
        boost::chrono::high_resolution_clock::time_point start = boost::chrono::high_resolution_clock::now();
        test_map_impl();
        boost::chrono::high_resolution_clock::time_point end = boost::chrono::high_resolution_clock::now();
        times_map(end - start);
    }

    std::cout << "Lambda runs took " << mean(times_lambda) << std::endl;
    std::cout << "Qi runs took " << mean(times_qi) << std::endl;
    std::cout << "Map runs took " << mean(times_map) << std::endl;
    return EXIT_SUCCESS;
}

令我惊讶的是,lambda确实很重要(一点点)。让我感到惊讶的是 更多的是qi实现慢得多。

Measure execution times for 100000 lambda runs
Measure execution times for 100000 qi runs
Measure execution times for 100000 map runs
Lambda runs took 12443 nanoseconds
Qi runs took 15311 nanoseconds
Map runs took 10466 nanoseconds

尝试优化#1

首先,我使用的是这样的符号

template <typename Iterator>
struct mimetype_matching_parser : qi::grammar<Iterator, std::string()> {

    mimetype_matching_parser() : mimetype_matching_parser::base_type(m_start,
            "mimetype_matching_parser") {

        m_mime_extensions.add
            (".htm", "text/html")
            (".html",  "text/html")
            (".php",  "text/html")
            (".css",  "text/css")
            (".svg",  "whatever...")
            ;

        using qi::no_case;

        m_start %= no_case[m_mime_extensions] >> qi::eoi;
    }

    qi::symbols<char, std::string>   m_mime_extensions;
    qi::rule<Iterator, std::string()> m_start;
};

直接将字符串作为属性返回。一位同事指出 这是一个额外的std :: string副本,所以我改了它,所以它只返回一个索引 static char数组:

const char *mime_literals[] = {
    "text/html",
    "text/css",
    "text/plain",
    // ... and so forth
};

template <typename Iterator>
struct mimetype_matching_parser : qi::grammar<Iterator, unsigned int()> {

    mimetype_matching_parser() : mimetype_matching_parser::base_type(m_start, "mimetype_matching_parser")
    {
        m_mime_extensions.add
            (".htm",0)
            (".html",0)
            (".php",0)
            (".css",1)
            (".svg",... etc.
            ;

        using qi::no_case;

        m_start %= no_case[m_mime_extensions] >> qi::eoi;
    }

    qi::symbols<char, unsigned int>   m_mime_extensions;
    qi::rule<Iterator, unsigned int()> m_start;
};

这有点快,但不值得一提。

在我的笔记本电脑上,在发布模式下,我得到:   - 野兽教程(Lambda)实现平均每次运行6200纳秒。   - Qi实现平均值约为7100纳秒。

现在,这将是我的第一个问题:为什么?

“野兽”的实施让我感到非常低效,因为它经历了所有的事情 子串,每次调用iequals,而它能够缓存 小写。

我认为qi肯定会对我添加到的关键字进行二元搜索 符号解析器。但看起来并不像。

尝试优化#2

所以我提出了我自己的,也很简单的实现,并使用静态地图 缓存小写临时(请参阅附加源中的impl3)。

测试结果:

  • 静态地图实施平均运行时间约为2400纳秒

Sooo,我想问题是为什么

我是否以某种方式滥用qi::symbols?它实际上是否进行二分查找 在其他地方失去了表现?

斯蒂芬¹

(我在Windows MSVC14 64位,增强1.66)

(¹这个问题是从Spirit General邮件列表中转述的,它发布于20180112T14:15CET; online archives似乎遗憾地被打破了)

1 个答案:

答案 0 :(得分:6)

12-01-18 14:15,Stephan Menzel写道:

  

所以我提出了两种不同的实现方式。请找到   来源附件。

我看过它。首先是一些肤浅的观察:

  1. 你比较苹果和梨,因为Beast使用零拷贝的字符串视图,其中Qi没有。

  2. 此外,样本选择会调用UB,因为uniform_int_distribution(0,10)超出了样本数组的范围(应该是(0, 9))。

  3. 最后,地图方法没有.txt扩展名的映射。

  4. 通过以下方法,我将测试程序简化/构建为以下内容:

    <强> Live On Coliru

    在我的系统上打印以下内容:

    Lambda runs took 2319 nanoseconds
    Qi     runs took 2841 nanoseconds
    Map    runs took 193 nanoseconds
    

    现在,最大的罪魁祸首(显然是?)每次通过循环(编译规则)都要构建语法。当然,没有必要。删除它会产生:

    <强> Live On Coliru

    Lambda runs took 2676 nanoseconds
    Qi     runs took 98 nanoseconds
    Map    runs took 189 nanoseconds
    

    即使你还没有真正需要它,你仍然会复制字符串。使用上面链接的答案的灵感,我可能写得像:

    #include <boost/spirit/include/qi.hpp>
    namespace qi_impl {
        namespace qi = boost::spirit::qi;
    
        struct mimetype_symbols_type : qi::symbols<char, char const*> {
            mimetype_symbols_type() {
                auto rev = [](string_view s) -> std::string { return { s.rbegin(), s.rend() }; };
    
                this->add
                    (rev(".htm"),  "text/html")
                    (rev(".html"), "text/html")
                    (rev(".php"),  "text/html")
                    (rev(".css"),  "text/css")
                    (rev(".txt"),  "text/plain")
                    (rev(".js"),   "application/javascript")
                    (rev(".json"), "application/json")
                    (rev(".xml"),  "application/xml")
                    (rev(".swf"),  "application/x-shockwave-flash")
                    (rev(".flv"),  "video/x-flv")
                    (rev(".png"),  "image/png")
                    (rev(".jpe"),  "image/jpeg")
                    (rev(".jpeg"), "image/jpeg")
                    (rev(".jpg"),  "image/jpeg")
                    (rev(".gif"),  "image/gif")
                    (rev(".bmp"),  "image/bmp")
                    (rev(".ico"),  "image/vnd.microsoft.icon")
                    (rev(".tiff"), "image/tiff")
                    (rev(".tif"),  "image/tiff")
                    (rev(".svg"),  "image/svg+xml")
                    (rev(".svgz"), "image/svg+xml")
                    ;
            }
        } static const mime_symbols;
    
        char const* using_spirit(const string_view &n_path) {
            char const* result = "application/text";
            qi::parse(n_path.crbegin(), n_path.crend(), qi::no_case[mime_symbols], result);
            return result;
        }
    }
    

    没有必要捣乱找到&#34;最后一个点&#34;首先,不需要检查最后的匹配&#34;,然后直接从符号中获取值。您可以根据需要自由分配到string_viewstd::string

    完整列表

    始终使用string_views(支持/显示std::string_viewboost::string_view)。

      

    另请注意,这显示了map<>方法中使用的自定义比较器,只是为了证明确实知道地图键都是小写的,这确实有好处。 (事实上​​,它不是因为它&#34;缓存了小写&#34;因为它只使用过一次!)

    <强> Live On Coliru

    #include <boost/chrono.hpp>
    #include <string>
    
    #ifdef BOOST_STRING_VIEW
        #include <boost/utility/string_view.hpp>
        using string_view = boost::string_view;
    #else
        #include <string_view>
        using string_view = std::string_view;
    #endif
    
    static auto constexpr npos = string_view::npos;
    
    #include <boost/spirit/include/qi.hpp>
    namespace qi_impl {
        namespace qi = boost::spirit::qi;
    
        struct mimetype_symbols_type : qi::symbols<char, char const*> {
            mimetype_symbols_type() {
                auto rev = [](string_view s) -> std::string { return { s.rbegin(), s.rend() }; };
    
                this->add
                    (rev(".htm"),  "text/html")
                    (rev(".html"), "text/html")
                    (rev(".php"),  "text/html")
                    (rev(".css"),  "text/css")
                    (rev(".txt"),  "text/plain")
                    (rev(".js"),   "application/javascript")
                    (rev(".json"), "application/json")
                    (rev(".xml"),  "application/xml")
                    (rev(".swf"),  "application/x-shockwave-flash")
                    (rev(".flv"),  "video/x-flv")
                    (rev(".png"),  "image/png")
                    (rev(".jpe"),  "image/jpeg")
                    (rev(".jpeg"), "image/jpeg")
                    (rev(".jpg"),  "image/jpeg")
                    (rev(".gif"),  "image/gif")
                    (rev(".bmp"),  "image/bmp")
                    (rev(".ico"),  "image/vnd.microsoft.icon")
                    (rev(".tiff"), "image/tiff")
                    (rev(".tif"),  "image/tiff")
                    (rev(".svg"),  "image/svg+xml")
                    (rev(".svgz"), "image/svg+xml")
                    ;
            }
        } static const mime_symbols;
    
        char const* using_spirit(const string_view &n_path) {
            char const* result = "application/text";
            qi::parse(n_path.crbegin(), n_path.crend(), qi::no_case[mime_symbols], result);
            return result;
        }
    }
    
    #include <boost/algorithm/string.hpp>
    namespace impl {
        string_view using_iequals(const string_view &n_path) {
    
            using boost::algorithm::iequals;
    
            auto const ext = [&n_path] {
                auto pos = n_path.rfind(".");
                return pos != npos? n_path.substr(pos) : string_view {};
            }();
    
            if (iequals(ext, ".htm"))  return "text/html";
            if (iequals(ext, ".html")) return "text/html";
            if (iequals(ext, ".php"))  return "text/html";
            if (iequals(ext, ".css"))  return "text/css";
            if (iequals(ext, ".txt"))  return "text/plain";
            if (iequals(ext, ".js"))   return "application/javascript";
            if (iequals(ext, ".json")) return "application/json";
            if (iequals(ext, ".xml"))  return "application/xml";
            if (iequals(ext, ".swf"))  return "application/x-shockwave-flash";
            if (iequals(ext, ".flv"))  return "video/x-flv";
            if (iequals(ext, ".png"))  return "image/png";
            if (iequals(ext, ".jpe"))  return "image/jpeg";
            if (iequals(ext, ".jpeg")) return "image/jpeg";
            if (iequals(ext, ".jpg"))  return "image/jpeg";
            if (iequals(ext, ".gif"))  return "image/gif";
            if (iequals(ext, ".bmp"))  return "image/bmp";
            if (iequals(ext, ".ico"))  return "image/vnd.microsoft.icon";
            if (iequals(ext, ".tiff")) return "image/tiff";
            if (iequals(ext, ".tif"))  return "image/tiff";
            if (iequals(ext, ".svg"))  return "image/svg+xml";
            if (iequals(ext, ".svgz")) return "image/svg+xml";
            return "application/text";
        }
    }
    
    #include <boost/algorithm/string.hpp>
    #include <map>
    
    namespace impl {
        struct CiCmp {
            template <typename R1, typename R2>
            bool operator()(R1 const& a, R2 const& b) const {
                return boost::algorithm::ilexicographical_compare(a, b);
            }
        };
    
        static const std::map<string_view, string_view, CiCmp> s_mime_exts_map  {
            { ".txt", "text/plain" },
            { ".htm",  "text/html" },
            { ".html", "text/html" },
            { ".php",  "text/html" },
            { ".css",  "text/css"  },
            { ".js",   "application/javascript" },
            { ".json", "application/json" },
            { ".xml",  "application/xml" },
            { ".swf",  "application/x-shockwave-flash" },
            { ".flv",  "video/x-flv" },
            { ".png",  "image/png" },
            { ".jpe",  "image/jpeg" },
            { ".jpeg", "image/jpeg" },
            { ".jpg",  "image/jpeg" },
            { ".gif",  "image/gif" },
            { ".bmp",  "image/bmp" },
            { ".ico",  "image/vnd.microsoft.icon" },
            { ".tif",  "image/tiff" },
            { ".tiff", "image/tiff" },
            { ".svg",  "image/svg+xml"},
            { ".svgz", "image/svg+xml"},
        };
    
        string_view using_map(const string_view& n_path) {
            auto const ext = [](string_view n_path) {
                auto pos = n_path.rfind(".");
                return pos != npos? n_path.substr(pos) : string_view {};
            };
    
            auto i = s_mime_exts_map.find(ext(n_path));
    
            if (i != s_mime_exts_map.cend()) {
                return i->second;
            } else {
                return "application/text";
            }
        }
    }
    
    #include <random>
    namespace samples {
    
        static string_view const s_samples[] = {
        "test.txt",
        "test.html",
        "longer/test.tiff",
        "www.webSite.de/ico.ico",
        "www.websIte.de/longEr/path/ico.bmp",
        "www.TEST.com/longer/path/ico.svg",
        "googlecom/shoRT/path/index.HTM",
        "googlecom/bild.jpg",
        "WWW.FLASH.COM/app.swf",
        "WWW.FLASH.COM/BILD.GIF"
        };
    
        std::mt19937 s_random_generator(std::random_device{}());
        std::uniform_int_distribution<> s_dis(0, boost::size(s_samples) - 1);
    
        string_view random_sample() {
            return s_samples[s_dis(s_random_generator)];
        }
    }
    
    #include <boost/functional/hash.hpp>
    #include <iostream>
    template <typename F>
    int generic_test(F f) {
        auto sample = samples::random_sample();
        string_view result = f(sample);
    
        //std::cout << "DEBUG " << sample << " -> " << result << "\n";
    
        return boost::hash_range(result.begin(), result.end());
    }
    
    #include <boost/serialization/array_wrapper.hpp> // missing include in boost version on coliru
    #include <boost/accumulators/accumulators.hpp>
    #include <boost/accumulators/statistics.hpp>
    
    template <typename F>
    auto benchmark(F f) {
        using C = boost::chrono::high_resolution_clock;
        using duration = C::duration;
    
        const unsigned int loops = 100000;
    
        namespace ba = boost::accumulators;
        ba::accumulator_set<duration, ba::features<ba::tag::mean>> times;
    
        for (unsigned int i = 0; i < loops; i++) {
            auto start = C::now();
            generic_test(f);
            times(C::now() - start);
        }
    
        return ba::mean(times);
    }
    
    
    int main() {
        std::cout << std::unitbuf;
        std::cout << "Lambda runs took " << benchmark(impl::using_iequals)   << std::endl;
        std::cout << "Qi     runs took " << benchmark(qi_impl::using_spirit) << std::endl;
        std::cout << "Map    runs took " << benchmark(impl::using_map)       << std::endl;
    }
    

    打印

    Lambda runs took 2470 nanoseconds
    Qi     runs took 119 nanoseconds
    Map    runs took 2239 nanoseconds // see Note above