7

问题

我知道Parsec并且uu-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)

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

有没有办法写类似pList(try(pa <* pb))in 的东西uu-parsinglib

例子

一个很好的例子是

pExample = pTest <* (pa <* pb)

和一个样本输入"ababab"

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

编辑

我们不能从 切换pList_ngpList,因为它不等同于Parsec版本。例如:

pExample2 = pTest <* pa

以及在使用 greedy 时"ababa"会成功Parsec但失败的示例输入。uu-parsinglibpList

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

一个小基准

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

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

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

让我们在两个库中制作解析器!

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

结果?uu-parsinglib300% 慢

uu-parsinglib - 3.19s
Parsec        - 1.04s

ghc -O3(用标志编译)

4

2 回答 2

10

要了解其中的微妙之处,重要的是要了解 Haskell 中的 try 构造与 uu-parsinglib 中使用的非贪婪解析策略之间的区别。实际上,后者是一种仅向前看一个符号的尝试。在这方面,它不如 Parsec 中的 try 构造强大,后者指定特定构造必须完全存在。然后是潜在的不同整体策略。Parsec 使用带有显式尝试提交的回溯策略,而 uu-parsinglib 使用带有偶尔单个符号前瞻的广度优先策略。

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

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

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

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

于 2014-01-19T01:09:15.537 回答
2

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

有关更多技术描述,请参阅Text.ParserCombinators.UU.README中的论文(可能还有之后的源代码)。在这里,我将使用pExample2草绘解析过程。分支发生在pList_ng(in pTest) 中,用于<|>识别空字符串或另一个元素。因为pTest后面是pa,而不是空字符串,解析另一个元素的替代方法实际上是识别单个'a'.

当我们在输入中看到第一个'a'时,两种选择都可以成功解析这个字符。接下来,我们看到一个'b'. 现在只解析一个的替代方案'a'无法取得任何进一步的进展,所以我们放弃了那个。剩下一个替代方案:识别 (a list of)'a'后跟'b'( pTest) 的替代方案。接下来是另一个'a',还有两种选择需要考虑。但随后我们看到了一个'b',而且,我们可以再次立即放弃第二个选择。然后是最后一个字符 an 'a',它再次表示两种选择。但是现在我们到了字符串的末尾,只有通过pa识别最终获得的替代方案a才能成功解析。

考虑到替代输入"ababab",我们看到pa当我们到达最终替代时,替代再次失败'b',所以只剩下pTest替代。那个完成是因为我们到达了最后,然后pa(接着)失败pTestpExample2,所以我们得到了一个错误。

'a'在任何时候,uu-parsinglib 只需要在内存中保留尚未失败的替代方案,并且广度优先策略确保所有替代方案都被同步评估,因此无需坚持第一个'b'直到结束到达字符串的长度并且在回溯之前它首先不匹配整个字符串。

编辑:

从我收集到的关于 Parsec 的信息来看,这确实效率较低,因为 Parsecpa直到pTest完成才考虑。try论文中的第 5.1 节对 uu-parsinglib 之类的东西说了几句话,包括一些反对意见。原始速度不是主要目标,我曾经看过一些基准的介绍,其中 uu-parsinglib 也没有名列前茅,但总体而言它表现相当不错。如果速度对于您提到的这个编译器非常重要,并且如果您不需要任何额外的功能,例如在线结果或纠错,也许您应该坚持使用 Parsec?(或者先寻找更全面的基准。)

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

好吧,您可以做的一件事是重写语法:

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

对于 Parsec,我认为会如下(但这似乎使它变慢):

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

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

 uu-parsinglib (pTest)  - 1.98s
 uu-parsinglib (pTest') - 1.11s
 Parsec (pTest)         - 0.59s
 Parsec (pTest')        - 0.67s
于 2013-12-30T21:50:44.273 回答