Pyparking packrat降低了性能



我正在寻找一种方法来提高我用pyparsing构建的解析器的性能。我阅读了packrat解析,这似乎真的有助于我的解析器的性能。然而,当我启用packrat解析时,性能变差了!如果没有packrat,解析一个20MB的文件大约需要2分钟。启用packrat后,所需时间将增加2-3倍。我读到packrat在parseActions方面可能有问题,所以我从语法中删除了所有的parseAction,看看packrat当时是否会提高性能,但这也没有帮助。我尝试了不同的缓存大小限制(不受限制,在100-1000的范围内),但当我启用packrat时,所有这些方法反而恶化了解析器的性能。

设置packrat的cache_size_limit有经验法则吗?有没有任何语法结构限制了packrat语法分析的使用,或者解释了为什么我的语法分析器的性能变得更差?

一个20MB的文件无论如何都需要一些时间来进行pyparking,但有些构造可能会减慢速度。

当我们重新设计packrat解析的实现以使用OrderedDict作为缓存时,我对缓存大小的不同值进行了一些测试。默认值128的效率仅比无边界缓存低1-2%,在内存和性能方面都取得了巨大的胜利。因此,如果超过200或300的值对缓存有很大帮助,同时在缓存搜索和内存管理方面付出代价,我会感到惊讶。

当您通过在输入字符串的相同位置重新访问相同的表达式来获得缓存命中时,Packrat解析效果最好。重要的是,它是完全相同的表达式,而不仅仅是一个等价的表达式。例如,如果你有这样的东西:

Literal("A") + Optional("," + Literal("B")) + Optional("," + Literal("C"))

分隔逗号的单独Literals是不同的表达式,因此不会被缓存命中。相反,如果您定义并重用单个表达式作为常见标点符号,则在packrat解析时,您有更好的机会从缓存中查找解析结果,而不是重新分析:

COMMA = Literal(",")
Literal("A") + Optional(COMMA + Literal("B")) + Optional(COMMA + Literal("C"))

最近,我一直在使用expr()语法来创建expr的副本,这样就不会意外地在全局范围内更改语法修饰符,如解析操作、空白设置等。这将反对表达式重用,因此,如果您有许多不需要的expr()实例,那么只需重用基本expr。还要注意,具有结果名称的表达式会生成隐式副本,因此请确保不要过度使用结果名称。

当使用infixNotation时,Packrat解析是最好的,尤其是当有6个或更多级别的运算符优先级时。infixNotation生成的表达式重用子表达式,而不是定义新的子表达式,因此能够获得更好的缓存性能。

当您可以对MatchFirst使用"|"运算符时,您可能对Or过度使用了"^"运算符。当在递归表达式(使用Forward)中使用时,这可能特别昂贵。比较这两个表达式:

arith_operand = integer_expr ^ float_expr ^ variable_name ^ Keyword("pi") ^ Keyword("e")

通过使用"^",将对所有表达式进行求值,然后选择最长的匹配项。特别是,如果解析"3.1416",这是必要的,因为前导的"3"将与integer匹配,但较长的"3.1416"将与float_expr匹配。但我们也尝试匹配一个变量名,以及关键字"pi"one_answers"e",它们保证不匹配。(variable_name、"pi"one_answers"e"也有同样的表达式屏蔽问题,因为它们可能作为一个变量名匹配。)但如果我们改为使用"|",那么我们的解析器将在第一个匹配时短路。我们只需要在分析事物的顺序上小心一点:

arith_operand = float_expr | integer_expr | Keyword("pi") | Keyword("e") | variable_name

也就是说,我们必须确保在匹配整数之前检查浮点上的匹配,因为浮点的前导部分可能会被误认为是整数。

如果你有容易相互排斥的替代品(比如一系列可能的关键词,由于它们的关键词性质,永远无法掩盖彼此),那么你也可以根据一些可能的频率对它们进行排序。例如:

bad_expr = Keyword("xylophone") | Keyword("the") | Keyword("a")

在大多数非音乐应用程序中可能会成为性能的失败者。

在pyparsing的早期,我没有Regex类,所以我必须使用Combine(Word(nums) + "." + Optional(Word(nums)))之类的东西为实数定义一个表达式。当您只需要使用Regex(r"d+.d*")时,这对于pyparsing来说是一项艰巨的工作。通常,实数表达式实际上是在给定的解析运行中使用和测试了数千次的低级表达式,因此转换为Regex确实有回报。或者使用pyparsing_common中的一个数字表达式,如pyparsing_common.realpyparsing_common.number。不过,您不需要太过火-pyparsing内部使用正则表达式来匹配使用WordoneOf的表达式。

您还可以直接检查缓存和缓存统计信息ParserElement.packrat_cacheParserElement.packrat_cache_stats。缓存统计数据是一个双元素列表,元素0是缓存命中数,元素1是未命中数。您可以为循环表达式定义一个调试操作,该操作将打印出缓存统计信息您还可以使用Counter:Counter(str(key[0]) for key in ParserElement.packrat_cache)查找重复的表达式。通过str处理表达式,它将有助于识别重复项因此,您可以查看不同缓存大小值的缓存效率。

编辑:我的错误是,对ParserElement.packrat_cache中的缓存密钥的迭代不起作用,缓存OrderedDict本身被隐藏起来,不受外部访问。

最新更新