26

减少 boost::spirit 编译时间的任何想法?

我刚刚移植了一个 flex 解析器来 boost::spirit。EBNF 有大约 25 条规则。

结果运行良好,运行时性能良好。

问题是编译需要永远!这大约需要十分钟,并且需要近千兆字节的内存。最初的 flex 解析器在几秒钟内编译完毕。

我正在使用 boost 版本 1.44.0 和 Visual Studio 2008。


在 Joel de Guzman 的文章“最佳实践”中,它说

具有复杂定义的规则严重损害了编译器。我们已经看到了超过 100 行长并且需要几分钟才能编译的规则

好吧,我没有那么长的东西,但是我的编译仍然需要几分钟以上

这是我语法中最复杂的部分。它是否适合在某种程度上被分解成更小的部分?

    rule
        =   (   tok.if_ >> condition  >> tok.then_ >> *sequel  )                            [ bind( &cRuleKit::AddRule, &myRulekit ) ]
        |   (   tok.if_ >> condition  >> tok.and_ >> condition >> tok.then_ >> *sequel  )   [ bind( &cRuleKit::AddRule, &myRulekit ) ]
        ;

    condition
        =   ( tok.identifier >> tok.oper_ >> tok.value )                                    [ bind( &cRuleKit::AddCondition, &myRulekit, _pass, _1, _2, _3 ) ]
        |   ( tok.identifier >> tok.between_ >> tok.value >> "," >> tok.value )             [ bind( &cRuleKit::AddConditionBetween, &myRulekit, _pass, _1, _3, _4 ) ]
        ;

    sequel
        =   ( tok.priority_ >> tok.high_ )      [ bind( &cRuleKit::setPriority, &myRulekit, 3 ) ]
        |   ( tok.priority_  )                  [ bind( &cRuleKit::setPriority, &myRulekit, 2 ) ]
        |   ( tok.interval_ >> tok.value )      [ bind( &cRuleKit::setInterval, &myRulekit, _2 ) ]
        |   ( tok.mp3_ >> tok.identifier )      [ bind( &cRuleKit::setMP3, &myRulekit, _2 ) ]
        |   ( tok.disable_ )                    [ bind( &cRuleKit::setNextRuleEnable, &myRulekit, false ) ]
        ;

通过注释掉部分语法,我发现了编译器花费最多时间的部分。

        set_reading
            =   tok.set_reading >> +attribute_reading
            ;

        attribute_reading
            =   ( tok.name_ >> tok.identifier )
                [
                                                bind( &cPackage::Add, &myReadings, _pass, _2 )
                ]
            |   ( tok.nmea_ >> tok.identifier )
                [
                                                bind( &cPackage::setNextNMEA, &myReadings, _2 )
                ]
            |   ( tok.column_ >> tok.integer )
                [
                                                bind( &cPackage::setNextColumn, &myReadings, _2 )
                ]
            |   ( tok.precision_ >> tok.value )
                [
                                                bind( &cPackage::setNextPrecision, &myReadings, _2 )
                ]
            |   ( tok.unit_ >> tok.identifier )
                [
                                                bind( &cPackage::setNextUnit, &myReadings, _2 )
                ]
            |   ( tok.value_ >> tok.identifier )
                [
                                                bind( &cPackage::setNextValue, &myReadings, _2 )
                ]
            |   ( tok.qualifier_ >> tok.identifier >> tok.qual_col_ >> tok.integer )
                [
                                                bind( &cPackage::setNextQualifier, &myReadings, _2, _4 )
                ]
            ;

我不会称它为复杂,但它肯定是最长的规则。所以我想我会尝试将其拆分,如下所示:

    set_reading
        =   tok.set_reading >> +attribute_reading
        ;

    attribute_reading
        =   attribute_reading_name
        |   attribute_reading_nmea
        |   attribute_reading_col
        |   attribute_reading_precision
        |   attribute_reading_unit
        |   attribute_reading_value
        |   attribute_reading_qualifier
        ;



    attribute_reading_name
        =   ( tok.name_ >> tok.identifier )     [ bind( &cPackage::Add, &myReadings, _pass, _2 ) ]
        ;
    attribute_reading_nmea
        =   ( tok.nmea_ >> tok.identifier )     [ bind( &cPackage::setNextNMEA, &myReadings, _2 ) ]
        ;
    attribute_reading_col
        =   ( tok.column_ >> tok.integer )      [ bind( &cPackage::setNextColumn, &myReadings, _2 ) ]
        ;
    attribute_reading_precision
        =   ( tok.precision_ >> tok.value )     [ bind( &cPackage::setNextPrecision, &myReadings, _2 ) ]
        ;
    attribute_reading_unit
        =   ( tok.unit_ >> tok.identifier )     [ bind( &cPackage::setNextUnit, &myReadings, _2 ) ]
        ;
    attribute_reading_value
        =   ( tok.value_ >> tok.identifier )    [ bind( &cPackage::setNextValue, &myReadings, _2 ) ]
        ;
    attribute_reading_qualifier
        =   ( tok.qualifier_ >> tok.identifier >> tok.qual_col_ >> tok.integer ) [ bind( &cPackage::setNextQualifier, &myReadings, _2, _4 ) ]

        ;

这从总编译时间中节省了几分钟!!!

奇怪的是,峰值内存需求保持不变,只是需要更少的时间

所以,我觉得我在学习 boost::spirit 方面的所有努力都将是值得的。

我确实认为编译器需要以这种方式进行如此仔细的引导有点奇怪。我原以为现代编译器会注意到该规则只是独立 OR 规则的列表。


我花了 7 天的大部分时间学习 boost::spirit 并从 flex 移植一个小而真实的解析器。我的结论是它可以工作并且代码非常优雅。不幸的是,简单地为实际应用程序扩展教程示例代码的天真使用很快就会使编译器负担过重——编译所花费的内存和时间变得完全不切实际。显然有一些技术可以解决这个问题,但它们需要我没有时间学习的神秘知识。我想我会坚持使用 flex ,这可能是丑陋和过时的,但相对简单且闪电般快速。

4

2 回答 2

17

我可以建议的一个技巧是将您的词法分析器和语法的构造函数的编译分开。实现这一点的最简单方法是在它们各自的头文件中只保留这些构造函数的声明,并将这些函数的定义移动到单独的翻译单元中。例如:

语法.hpp:

template <typename Iterator>
struct grammar : qi::grammar<Iterator>
{
    grammar();   // declaration only
    // ...
};

语法定义.hpp:

// This file should not contain anything else.
#include "grammar.hpp"

// Definition of constructor.
template <typename Iterator>
grammar<Iterator>::grammar()
{
    // initialize your rules here
}

语法.cpp:

// This file should not contain anything else.
#include "grammar_def.hpp"

// Explicitly instantiate the constructor for the iterator type
// you use to invoke the grammar (here, as an example I use 
// std::string::const_iterator).
typedef std::string::const_iterator iterator_type;
template grammar<iterator_type>::grammar();

对词法分析器对象做同样的事情。

这种方法比直接方法需要更多的工作,但它允许为整个编译分配内存和时间要求。这种方法的另一个优点是语法构造函数中的任何更改都不需要重新编译文件以外的任何内容grammar.cpp

给词法分析器的另一个建议:尽量减少token_def<>实例的使用。token_def<>只有在解析过程中要将令牌值作为属性访问时才需要使用。在所有其他情况下,您可能会逃脱lex::stringlex::char_定义您的令牌。

于 2011-03-31T22:52:17.403 回答
13

我必须得出结论,boost:spirit 虽然很优雅,但对于许多现实世界的解析问题来说并不是一个可行的选择,因为即使是专家也无法解决的冗长的编译时间。

通常最好坚持使用 flex 之类的东西,这可能是丑陋和过时的,但相对简单且闪电般快速。

作为我认为的“现实世界”问题的一个例子,一个解析器最重要部分的铁路图可以在几秒钟内编译,但是 boost:spirit 在十分钟后仍在继续运行

在此处输入图像描述

于 2012-07-20T18:54:44.740 回答