【问题标题】:Minimizing boost::spirit compile times最小化 boost::spirit 编译时间
【发布时间】:2011-07-27 04:11:33
【问题描述】:

有什么减少 boost::spirit 编译时间的想法吗?

我刚刚将一个弹性解析器移植到 boost::spirit。 EBNF 有大约 25 条规则。

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

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

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


在 Joel de Guzman 的文章 'Best Practices' 中说

具有复杂定义的规则严重损害了编译器。我们见过 一百多条规则 行很长,需要几个 编译时间

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

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

    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,它可能是丑陋且过时的,但相对简单且快速。

【问题讨论】:

    标签: c++ boost boost-spirit


    【解决方案1】:

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

    语法.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&lt;&gt; 实例的使用。只有在解析过程中要将令牌值作为属性访问时,才需要使用token_def&lt;&gt;。在所有其他情况下,您可能会使用lex::stringlex::char_ 来定义您的令牌。

    【讨论】:

    • 拜托,你能指出一个使用 lex::string 定义令牌的例子吗?当我简单地将 token_def 替换为 lex::string 时出现编译器错误
    • 在grammar_def.hpp中你写“这个文件不应该包含其他任何东西”。规则的定义(但不是声明)可能需要的文件,例如 phoenix_core.hpp、phoenix_operator.hpp?
    【解决方案2】:

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

    通常最好坚持使用 flex 之类的东西,它可能很丑陋且过时,但相对简单且速度极快。

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

    【讨论】:

    • 感谢您提供结论,尽管它可能不受欢迎。这对我很有用,因为我正在尝试使用精神。
    • @ravenspoint 大约 20 次后,我遇到了这个答案,我冒昧地改变了你的措辞,这样它就不再是不必要的“绝对”了。在结果形式中,我可以借出我的 +1。我喜欢 Spirit,但我肯定不会在每项解析工作中都使用它
    • @sehe 我很好奇 - 你真的用精神来解决现实世界的解析问题吗,说至少有 20 条规则?你的编译需要多长时间?
    • @ravenspoint 我的语法编译时间从 20 秒到 2 分钟不等,但这并不重要,因为我很少编译解析器。我有构建规则,ccache/pch(如果适用)。我的 {refactor/[build]/test}/greenbar 构建通常需要 10 秒。
    • @ravenspoint 我想这是我的转折点:我以敏捷的方式使用 Spirit。我很少花很多时间开发解析器。不过,当我使用 CoCo/R 和 flex 时,我曾经使用过。这基本上是因为我将解析与处理代码混合在一起。你可以说我已经“调整”了我的工作流程,但我并不后悔。这也意味着如果您还没有(非常)体验 Spirit 可能会非常令人沮丧:(
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-05-13
    • 2011-07-23
    • 2015-01-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多