Pyparsing packrat 会降低性能
Pyparsing packrat slows down performance
我正在寻找一种方法来提高我使用 pyparsing
构建的解析器的性能。我阅读了有关 Packrat 解析的内容,这似乎真的有助于提高我的解析器的性能。但是,当我启用 Packrat 解析时,性能变得更差了!如果没有 packrat,解析一个 20 MB 的文件大约需要 2 分钟。启用 packrat 后,它需要多 2-3 倍的时间。我读到 packrat 可能会遇到 parseActions 问题,所以我从我的语法中删除了所有 parseActions 以查看 packrat 是否会提高其性能,但这也没有帮助。我尝试了不同的缓存大小限制(无限制且在 100-1000 范围内),但所有这些方法反而在我启用 packrat 时恶化了解析器的性能。
设置 packrat 的 cache_size_limit 有经验法则吗?是否有任何语法结构限制使用 packrat 解析或解释为什么我的解析器的性能变差?
一个 20MB 的文件无论如何都需要 pyparsing 一些时间,但有些结构可能会减慢你的速度。
当我们重新实现 packrat 解析以使用 OrderedDict 作为缓存时,我对缓存大小的不同值进行了一些测试。结果表明,默认值 128 的效率仅比无界缓存低 1-2%,但在内存和性能方面都取得了巨大的进步。因此,如果值超过 200 或 300 会对缓存有很大帮助,同时在缓存搜索和内存管理方面付出代价,我会感到惊讶。
当您通过在输入字符串的相同位置重新访问相同的表达式来获得缓存命中时,Packrat 解析效果最佳。重要的是它是完全相同的 same 表达式,而不仅仅是等价的表达式。例如,如果你有类似的东西:
Literal("A") + Optional("," + Literal("B")) + Optional("," + Literal("C"))
分隔逗号的单独文字是不同的表达式,因此不会被缓存命中。相反,如果您为常用标点符号定义并重用单个表达式,则 Packrat 解析更有可能从缓存中查找其解析结果,而不是重新解析:
COMMA = Literal(",")
Literal("A") + Optional(COMMA + Literal("B")) + Optional(COMMA + Literal("C"))
最近我一直在使用 expr()
语法来创建 expr
的副本,这样解析操作、空格设置等修饰符就不会在全局范围内意外更改。这将不利于表达式重用,因此如果您有许多不需要的 expr()
实例,则只需重用基础 expr
。另请注意,带有结果名称的表达式会生成隐式副本,因此请务必不要过度使用结果名称。
Packrat 解析在使用 infixNotation
时效果最好,尤其是当有 6 级或更多运算符优先级时。 infixNotation
生成的表达式重用子表达式而不是定义新的,因此能够获得更好的缓存性能。
当您可以使用“|”时,您可能过度使用了 Or
的“^”运算符MatchFirst
的运算符。在递归表达式中使用时(使用 Forward
),这可能会特别昂贵。比较这两个表达式:
arith_operand = integer_expr ^ float_expr ^ variable_name ^ Keyword("pi") ^ Keyword("e")
通过使用“^”,所有表达式都将被计算,然后选择最长的匹配项。特别是,如果解析“3.1416”,这是必要的,因为前导“3”将匹配 integer
,但较长的“3.1416”将匹配 float_expr
。但我们也尝试匹配一个变量名,以及关键字 "pi" 和 "e",保证不会匹配。 (我们对 variable_name
和 "pi" 以及 "e" 有相同的表达式掩码问题,因为它们可能会匹配为可能的变量名。)但是如果我们改为使用“|”,那么我们的解析器将在第一次匹配时短路。我们只需要注意解析的顺序即可:
arith_operand = float_expr | integer_expr | Keyword("pi") | Keyword("e") | variable_name
也就是说,我们必须确保在匹配整数之前检查浮点数是否匹配,因为浮点数的前导部分可能会被误认为是整数。
如果您有很容易相互排斥的备选方案(例如一系列可能的关键字,由于它们的关键字特性,它们永远不会相互掩盖),那么您也可以根据一些可能的频率对它们进行排序。例如:
bad_expr = Keyword("xylophone") | Keyword("the") | Keyword("a")
在大多数非音乐应用程序中可能会降低性能。
在 pyparsing 的早期,我没有 Regex
class,所以我必须使用 Combine(Word(nums) + "." + Optional(Word(nums)))
之类的东西定义一个实数表达式。当你可以只使用 Regex(r"\d+\.\d*")
时,对于 pyparsing 来说这是很多工作。通常,实数表达式是真正的低级表达式,在给定的解析 运行 中使用和测试了数千次,因此转换为 Regex
确实值得。或者使用 pyparsing_common
中的数字表达式之一,例如 pyparsing_common.real
或 pyparsing_common.number
。你不需要过火 - pyparsing 在内部使用正则表达式来匹配使用 Word
和 oneOf
.
的表达式
您还可以直接检查缓存和缓存统计信息,ParserElement.packrat_cache
和 ParserElement.packrat_cache_stats
。缓存统计信息是一个包含两个元素的列表,元素 0 是缓存命中数,元素 1 是未命中数。您可以为将打印出缓存统计信息的重复表达式定义调试操作。 您还可以使用计数器查找重复的表达式:Counter(str(key[0]) for key in ParserElement.packrat_cache)
。通过 str
-ing 表达式,将有助于识别重复项。 因此您可以查看不同缓存大小值的缓存效率。
编辑:我的错误,在 ParserElement.packrat_cache
中对缓存键的迭代不起作用,缓存 OrderedDict
本身是隐藏的,远离外部访问。
我正在寻找一种方法来提高我使用 pyparsing
构建的解析器的性能。我阅读了有关 Packrat 解析的内容,这似乎真的有助于提高我的解析器的性能。但是,当我启用 Packrat 解析时,性能变得更差了!如果没有 packrat,解析一个 20 MB 的文件大约需要 2 分钟。启用 packrat 后,它需要多 2-3 倍的时间。我读到 packrat 可能会遇到 parseActions 问题,所以我从我的语法中删除了所有 parseActions 以查看 packrat 是否会提高其性能,但这也没有帮助。我尝试了不同的缓存大小限制(无限制且在 100-1000 范围内),但所有这些方法反而在我启用 packrat 时恶化了解析器的性能。
设置 packrat 的 cache_size_limit 有经验法则吗?是否有任何语法结构限制使用 packrat 解析或解释为什么我的解析器的性能变差?
一个 20MB 的文件无论如何都需要 pyparsing 一些时间,但有些结构可能会减慢你的速度。
当我们重新实现 packrat 解析以使用 OrderedDict 作为缓存时,我对缓存大小的不同值进行了一些测试。结果表明,默认值 128 的效率仅比无界缓存低 1-2%,但在内存和性能方面都取得了巨大的进步。因此,如果值超过 200 或 300 会对缓存有很大帮助,同时在缓存搜索和内存管理方面付出代价,我会感到惊讶。
当您通过在输入字符串的相同位置重新访问相同的表达式来获得缓存命中时,Packrat 解析效果最佳。重要的是它是完全相同的 same 表达式,而不仅仅是等价的表达式。例如,如果你有类似的东西:
Literal("A") + Optional("," + Literal("B")) + Optional("," + Literal("C"))
分隔逗号的单独文字是不同的表达式,因此不会被缓存命中。相反,如果您为常用标点符号定义并重用单个表达式,则 Packrat 解析更有可能从缓存中查找其解析结果,而不是重新解析:
COMMA = Literal(",")
Literal("A") + Optional(COMMA + Literal("B")) + Optional(COMMA + Literal("C"))
最近我一直在使用 expr()
语法来创建 expr
的副本,这样解析操作、空格设置等修饰符就不会在全局范围内意外更改。这将不利于表达式重用,因此如果您有许多不需要的 expr()
实例,则只需重用基础 expr
。另请注意,带有结果名称的表达式会生成隐式副本,因此请务必不要过度使用结果名称。
Packrat 解析在使用 infixNotation
时效果最好,尤其是当有 6 级或更多运算符优先级时。 infixNotation
生成的表达式重用子表达式而不是定义新的,因此能够获得更好的缓存性能。
当您可以使用“|”时,您可能过度使用了 Or
的“^”运算符MatchFirst
的运算符。在递归表达式中使用时(使用 Forward
),这可能会特别昂贵。比较这两个表达式:
arith_operand = integer_expr ^ float_expr ^ variable_name ^ Keyword("pi") ^ Keyword("e")
通过使用“^”,所有表达式都将被计算,然后选择最长的匹配项。特别是,如果解析“3.1416”,这是必要的,因为前导“3”将匹配 integer
,但较长的“3.1416”将匹配 float_expr
。但我们也尝试匹配一个变量名,以及关键字 "pi" 和 "e",保证不会匹配。 (我们对 variable_name
和 "pi" 以及 "e" 有相同的表达式掩码问题,因为它们可能会匹配为可能的变量名。)但是如果我们改为使用“|”,那么我们的解析器将在第一次匹配时短路。我们只需要注意解析的顺序即可:
arith_operand = float_expr | integer_expr | Keyword("pi") | Keyword("e") | variable_name
也就是说,我们必须确保在匹配整数之前检查浮点数是否匹配,因为浮点数的前导部分可能会被误认为是整数。
如果您有很容易相互排斥的备选方案(例如一系列可能的关键字,由于它们的关键字特性,它们永远不会相互掩盖),那么您也可以根据一些可能的频率对它们进行排序。例如:
bad_expr = Keyword("xylophone") | Keyword("the") | Keyword("a")
在大多数非音乐应用程序中可能会降低性能。
在 pyparsing 的早期,我没有 Regex
class,所以我必须使用 Combine(Word(nums) + "." + Optional(Word(nums)))
之类的东西定义一个实数表达式。当你可以只使用 Regex(r"\d+\.\d*")
时,对于 pyparsing 来说这是很多工作。通常,实数表达式是真正的低级表达式,在给定的解析 运行 中使用和测试了数千次,因此转换为 Regex
确实值得。或者使用 pyparsing_common
中的数字表达式之一,例如 pyparsing_common.real
或 pyparsing_common.number
。你不需要过火 - pyparsing 在内部使用正则表达式来匹配使用 Word
和 oneOf
.
您还可以直接检查缓存和缓存统计信息,ParserElement.packrat_cache
和 ParserElement.packrat_cache_stats
。缓存统计信息是一个包含两个元素的列表,元素 0 是缓存命中数,元素 1 是未命中数。您可以为将打印出缓存统计信息的重复表达式定义调试操作。 您还可以使用计数器查找重复的表达式: 因此您可以查看不同缓存大小值的缓存效率。Counter(str(key[0]) for key in ParserElement.packrat_cache)
。通过 str
-ing 表达式,将有助于识别重复项。
编辑:我的错误,在 ParserElement.packrat_cache
中对缓存键的迭代不起作用,缓存 OrderedDict
本身是隐藏的,远离外部访问。