uu-parsinglib的性能与Parsec中的“try”相比

时间:2013-12-30 19:50:10

标签: performance parsing haskell parsec uu-parsinglib

问题

我知道Parsecuu-parsinglib,我在两者中都编写过解析器。最近我发现,uu-parsinglib中存在一个问题,可能会对其性能产生重大影响,我看不到解决问题的方法。

让我们考虑使用Parsec解析器:

pa = char 'a'
pb = char 'b'
pTest = many $ try(pa <* pb)

uu-parsinglib中的等价物是什么? 如下:

pa = pSym 'a'
pb = pSym 'b'
pTest = pList_ng (pa <* pb)

不同之处在于,在Parsecmany会吃(pa <* pb)"ab"对)贪婪直到它不再匹配,而在uu-parsinglib },pList_ng并不贪心,因此在解析每个(pa <* pb)后,它会在内存中保留可能的回溯方式。

有没有办法在pList(try(pa <* pb))中编写类似uu-parsinglib的内容?

示例

一个很好的例子是

pExample = pTest <* (pa <* pb)

以及"ababab"的示例输入。

使用Parsec,我们会收到错误(因为pTest是贪婪解析"ab"对),但在uu-parsinglib中,字符串将被解析而没有任何问题。

修改

我们无法从pList_ng切换到pList,因为它不等同于Parsec版本。例如:

pExample2 = pTest <* pa

并且"ababa"的示例输入在Parsec中会成功,但在使用贪婪uu-parsinglib时会在{​​{1}}中失败。

如果我们在这里使用pListuu-parsinglib当然会成功,但对于某些输入和规则来说可能会慢得多。例如,考虑输入pList_ng"ababab"只会失败,因为Parsec会消耗整个字符串而pTest将无法匹配。 pa也将失败,但检查更多步骤 - 它将匹配整个字符串并失败,然后丢弃最后uu-parsinglib对并再次失败,等等。如果我们有一些嵌套规则和有趣的输入文本,它会产生巨大的差异。

一点基准

为了证明问题存在于实际中,请考虑遵循语法(在伪代码中 - 但我认为它非常直观):

"ab"

因此,作为我们程序的输入,我们得到一个包含模式的字符串,例如“abababaaba”包含2个模式“abababa”和“aba”。

让我们在两个库中创建解析器!

pattern = many("ab") + "a" input = many(pattern)

uu-parsinglib

import Data.Char import qualified Text.ParserCombinators.UU as UU import Text.ParserCombinators.UU hiding(parse) import Text.ParserCombinators.UU.BasicInstances hiding (Parser) import System.TimeIt (timeIt) pa = pSym 'a' pb = pSym 'b' pTest = pList $ pList_ng ((\x y -> [x,y]) <$> pa <*> pb) <* pa main :: IO () main = do timeIt maininner return () maininner = do let (x,e) = UU.parse ((,) <$> pTest <*> pEnd) (createStr (LineColPos 0 0 0) (concat $ replicate 1000 (concat (replicate 1000 "ab") ++ "a"))) print $ length x

Parsec

结果? import Control.Applicative import Text.Parsec hiding (many, optional, parse, (<|>)) import qualified Text.Parsec as Parsec import System.TimeIt (timeIt) pa = char 'a' pb = char 'b' pTest = many $ many (try ((\x y -> [x,y]) <$> pa <*> pb)) <* pa main :: IO () main = do timeIt maininner2 return () maininner2 = do let Right x = Parsec.runParser pTest (0::Int) "test" $ (concat $ replicate 1000 (concat (replicate 1000 "ab") ++ "a")) print $ length x &gt;慢300%

uu-parsinglib

(使用uu-parsinglib - 3.19s Parsec - 1.04s 标志编译)

2 个答案:

答案 0 :(得分:9)

要理解细微之处,了解Haskell中的try构造与uu-parsinglib中使用的非贪婪解析策略之间的差异非常重要。实际上后者是一个尝试,只是向前看一个符号。在这方面,它不如Parsec的try构造强大,在Parsec中,您指定必须完全存在特定构造。然后是潜在的不同整体战略。 Parsec使用具有显式尝试提交的反向跟踪策略,而uu-parsinglib使用广度优先策略,偶尔使用单个符号前瞻。

因此两者之间存在时差并不令人意外。在Parsec案例中,在看到完整构造(两个符号)之后,确定尝试的替代方案确实适用,而贪婪的uu-parsinglib在成功看到第一个符号后决定这必须是正确的替代方案。而这个结论可能是不合理的。

如果一个人转向广度优先策略,uu-parsinglib使用一个必须同时跟踪几个替代方案,这需要时间。两种选择,两倍的时间等。

Parsec的优势在于你可以通过自由使用try结构和以正确的顺序放置备选方案来调整反向跟踪解析器,但是你也更有可能在邮件列表上询问你的解析器为什么不这样做按预期工作。你不是在编写语法来编写解析器。 uu-parsinglib从频谱的另一端开始:我们尝试接受相当多的语法集合(以及它们隐含的解析器)。

我的感觉是,在存在具有出色错误修复解析器的try构造时要复杂得多。一旦尝试构造失败,就不可能回到那里并决定通过一个小的修正,它比它之后的替代方案好得多。

答案 1 :(得分:2)

您正在描述的行为(使用pList_ng)确实适用于其他解析器(例如,在Jeroen Fokker的Functional Parsers组合器中描述的简单成功列表方法),但不是uu-parsinglib。该库使用广度优先策略来避免空间泄漏(由于挂在整个输入上,就像使用深度优先策略时那样)。这就是为什么我问你是创建了一个测试用例还是看过内部组件。

有关更多技术说明,请参阅Text.ParserCombinators.UU.README中的文章(以及之后的源代码)。在这里,我将使用pExample2草绘解析过程。分支发生在pList_ngpTest)中,它使用<|>来识别空字符串或其他元素。因为pTest后跟pa,而不是空字符串,解析另一个元素的替代方法实际上是识别单个'a'

当我们在输入中看到第一个'a'时,两个备选方案都可以成功解析此字符。接下来,我们看到'b'。现在只解析单个'a'的替代方法无法取得任何进一步的进展,因此我们放弃了那个。剩下一个替代方案:识别({1}}后跟'a''b')的列表。接下来是另一个pTest,还有两个备选方案需要考虑。但后来我们看到'a',我们再次可以立即放弃第二种选择。然后是最后一个characer,'b',再一次意味着两个选择。但现在我们到达字符串的末尾,只有通过让'a'识别最终pa获得的替代方案才能导致成功的解析。

考虑到替代输入a,当我们到达最终"ababab"时,我们发现pa替代方案再次失败,因此只剩下'b'替代方案。那个结束,因为我们到达终点,然后pTest(在papTest之后)失败,所以我们得到一个错误。

在任何时候,uu-parsinglib只需要在内存中保留尚未失败的备选方案,而广度优先策略确保以锁步方式评估所有备选方案,因此无需挂起第一个{ {1}}和pExample2直到字符串结束,并且在回溯之前它与整个字符串不匹配。

修改

从我收集的关于Parsec的内容来看,这确实效率较低,因为Parsec在'a'完成之前不会考虑'b'。本文的第5.1节说明了关于uu-parsinglib的pa之类的内容,包括一些异议。原始速度不是主要目标,我曾经看过一些基准测试的演示文稿,其中uu-parsinglib也没有出现在顶部,但整体而言它表现得相当不错。如果速度对于你提到的这个编译器如此重要,如果你不需要任何额外的功能,如在线结果或纠错,也许你应该坚持Parsec? (或者首先寻找更全面的基准测试。)

这两个库之间存在明显的差异,所以我并不惊讶Parsec在某些情况下更快,尽管这种情况的差异确实很大。也许有一种方法可以进一步调整uu-parsinglib版本(不会改变内容,正如本节中关于贪婪解析的部分所暗示的那样),但这对我来说并不明显(无论如何)。

嗯,你能做的一件事就是重写语法:

pTest

对于Parsec而言,我认为(但这似乎会让它变慢):

try

这有帮助,但还不足以击败Parsec版本。使用您的基准测试,我得到以下结果:

pTest' = pList $ pa *> pList ((\x y -> [x,y]) <$$> pb <*> pa)