Haskell 解析器组合器能变得高效吗?

Haskell 解析器组合器能变得高效吗?,haskell,f#,parser-generator,parser-combinators,parsec,Haskell,F#,Parser Generator,Parser Combinators,Parsec,大约6年前,我在OCaml中对自己的解析器组合器进行了基准测试,发现它们比当时提供的解析器生成器慢约5倍。我最近重新探讨了这一主题,并对Haskell的Parsec与用F#编写的简单手卷进行了基准测试,惊讶地发现F#比Haskell快25倍 下面是我用来从文件中读取大型数学表达式、解析和计算的Haskell代码: import Control.Applicative import Text.Parsec hiding ((<|>)) expr = chainl1 term ((+)

大约6年前,我在OCaml中对自己的解析器组合器进行了基准测试,发现它们比当时提供的解析器生成器慢约5倍。我最近重新探讨了这一主题,并对Haskell的Parsec与用F#编写的简单手卷进行了基准测试,惊讶地发现F#比Haskell快25倍

下面是我用来从文件中读取大型数学表达式、解析和计算的Haskell代码:

import Control.Applicative
import Text.Parsec hiding ((<|>))

expr = chainl1 term ((+) <$ char '+' <|> (-) <$ char '-')

term = chainl1 fact ((*) <$ char '*' <|> div <$ char '/')

fact = read <$> many1 digit <|> char '(' *> expr <* char ')'

eval :: String -> Int
eval = either (error . show) id . parse expr "" . filter (/= ' ')

main :: IO ()
main = do
    file <- readFile "expr"
    putStr $ show $ eval file
    putStr "\n"

您是否尝试过已知的快速解析器库之一?Parsec的目标从来不是速度,而是易用性和清晰性。与类似attoparsec的比较可能更公平,特别是因为字符串类型可能更相等(
ByteString
,而不是
string


我还想知道使用了哪些编译标志。这是臭名昭著的Jon Harrop的另一篇推特文章,如果Haskell代码没有进行任何优化,我也不会感到惊讶。

我想出了一个Haskell解决方案,它比您发布的Haskell解决方案(使用我调制的测试表达式)快30倍

主要变化:

  • 将Parsec/String更改为Attoparsec/ByteString
  • 事实
    功能中,将
    读取
    多个1位数
    更改为
    十进制
  • 使
    chainl1
    递归严格(对于更懒惰的版本,删除$)
  • 我试着让你拥有的一切尽可能相似

    import Control.Applicative
    import Data.Attoparsec
    import Data.Attoparsec.Char8
    import qualified Data.ByteString.Char8 as B
    
    expr :: Parser Int
    expr = chainl1 term ((+) <$ char '+' <|> (-) <$ char '-')
    
    term :: Parser Int
    term = chainl1 fact ((*) <$ char '*' <|> div <$ char '/')
    
    fact :: Parser Int
    fact = decimal <|> char '(' *> expr <* char ')'
    
    eval :: B.ByteString -> Int
    eval = either (error . show) id . eitherResult . parse expr . B.filter (/= ' ')
    
    chainl1 :: (Monad f, Alternative f) => f a -> f (a -> a -> a) -> f a
    chainl1 p op = p >>= rest where
      rest x = do f <- op
                  y <- p
                  rest $! (f x y)
               <|> pure x
    
    main :: IO ()
    main = B.readFile "expr" >>= (print . eval)
    
    导入控件。应用程序
    导入数据
    导入Data.c.Char8
    将限定数据.ByteString.Char8作为B导入
    expr::Parser Int
    expr=chainl1术语(+)a->a)->f a
    chainl1 p op=p>>=静止位置
    rest x=do f=(print.eval)
    
    我想我从中得出的结论是,解析器组合器的大部分减速是因为它的基础效率低下,而不是因为它本身就是解析器组合器

    我想,随着时间的推移,分析速度可能会加快,因为当我超过25×标记时,我停止了分析


    我不知道这是否会比移植到Haskell的优先级提升解析器更快。也许这将是一个有趣的测试?

    简而言之,解析器组合器在词法分析方面很慢

    有一个Haskell combinator库用于构建lexer(请参阅Manuel M.T.Chakravarty的“Lazy Lexing is Fast”)——由于表是在运行时生成的,因此没有代码生成的麻烦。这个库被使用了一点——它最初是在一个FFI预处理器中使用的,但我认为它从来没有被上传到Hackage,所以对于常规使用来说可能有点太不方便了


    在上面的OCaml代码中,解析器直接匹配字符列表,因此它可以与宿主语言中的列表分解一样快(如果在Haskell中重新实现,它将比Parsec快得多)。Christian Lindig有一个OCaml库,其中有一组解析器组合器和一组lexer组合器-lexer组合器肯定比Manuel Chakravarty的简单得多,在编写lexer生成器之前,可能值得跟踪这个库并对其进行基准测试。

    我目前正在开发下一个版本的FParsec(v.0.9),在许多情况下,它可以将性能提高2倍

    [更新:FParsec 0.9已发布,请参阅]

    我已经针对两个FParsec实现测试了Jon的F#解析器实现。第一个FParsec解析器是djahandarie解析器的直接翻译。第二个使用FParsec的可嵌入运算符优先级组件。作为输入,我使用了一个由Jon的OCaml脚本生成的字符串,该脚本的参数为10,输入大小约为2.66MB。所有解析器都在发布模式下编译,并在32位.NET 4 CLR上运行。我只测量了纯解析时间,没有包括启动时间或构造输入字符串(对于FParsec解析器)或字符列表(Jon的解析器)所需的时间

    我测量了以下数字(帕伦斯0.9版的更新数字):

    • 乔恩的手摇解析器:~230ms
    • FParsec解析器#1:~270ms(~235ms)
    • FParsec解析器#2:~110ms(~102ms)
    根据这些数字,我认为解析器组合器绝对可以提供有竞争力的性能,至少对于这个特定的问题,特别是如果您考虑到FParsec

    • 自动生成高度可读的错误消息
    • 支持非常大的文件作为输入(具有任意回溯),以及
    • 附带一个声明性的、运行时可配置的运算符优先解析器模块
    以下是两个FParsec实现的代码:

    解析器#1(djahandarie解析器的翻译):

    打开FParsec
    设str s=pstring s
    让expr,exprRef=createParserForwardedToRef()
    让fact=pint32介于(str)(“”(str”))expr之间
    let term=chainl1事实((str“*”>>%(*))(str”/“>>%(/))
    do exprRef:=CHAINEL1项((str“+”>>%(+))(str“-”>>%(-))
    让parse str=run expr str
    
    解析器#2(惯用FParsec实现):

    打开FParsec
    设opp=new operatorreceidenceparser()
    类型Assoc=关联性
    设str s=pstring s
    设noWS=preturn()//伪空白解析器
    opp.AddOperator(InfixOperator(“-”,noWS,1,Assoc.Left,(-))
    opp.AddOperator(InfixOperator(“+”,noWS,1,关联左,(+))
    opp.AddOperator(InfixOperator(“*”,noWS,2,Assoc.Left,(*))
    opp.AddOperator(InfixOperator(“/”,noWS,2,Assoc.Left,(/))
    让expr=opp.ExpressionParser
    让term=pint32介于(str)(“”(str”))expr之间
    
    opp.TermParser我一直觉得解析器组合器效率很低,但你必须用同一种语言尝试两种解决方案,以获得速度差异的良好衡量。你似乎使用的是parsec 3.x,根据这一点,它比parsec 2慢。这可能也是一个问题
    open Printf
    
    let rec f ff n =
      if n=0 then fprintf ff "1" else
        fprintf ff "%a+%a*(%a-%a)" f (n-1) f (n-1) f (n-1) f (n-1)
    
    let () =
      let n = try int_of_string Sys.argv.(1) with _ -> 3 in
      fprintf stdout "%a\n" f n
    
    import Control.Applicative
    import Data.Attoparsec
    import Data.Attoparsec.Char8
    import qualified Data.ByteString.Char8 as B
    
    expr :: Parser Int
    expr = chainl1 term ((+) <$ char '+' <|> (-) <$ char '-')
    
    term :: Parser Int
    term = chainl1 fact ((*) <$ char '*' <|> div <$ char '/')
    
    fact :: Parser Int
    fact = decimal <|> char '(' *> expr <* char ')'
    
    eval :: B.ByteString -> Int
    eval = either (error . show) id . eitherResult . parse expr . B.filter (/= ' ')
    
    chainl1 :: (Monad f, Alternative f) => f a -> f (a -> a -> a) -> f a
    chainl1 p op = p >>= rest where
      rest x = do f <- op
                  y <- p
                  rest $! (f x y)
               <|> pure x
    
    main :: IO ()
    main = B.readFile "expr" >>= (print . eval)
    
    open FParsec
    
    let str s = pstring s
    let expr, exprRef = createParserForwardedToRef()
    
    let fact = pint32 <|> between (str "(") (str ")") expr
    let term =   chainl1 fact ((str "*" >>% (*)) <|> (str "/" >>% (/)))
    do exprRef:= chainl1 term ((str "+" >>% (+)) <|> (str "-" >>% (-)))
    
    let parse str = run expr str
    
    open FParsec
    
    let opp = new OperatorPrecedenceParser<_,_,_>()
    type Assoc = Associativity
    
    let str s = pstring s
    let noWS = preturn () // dummy whitespace parser
    
    opp.AddOperator(InfixOperator("-", noWS, 1, Assoc.Left, (-)))
    opp.AddOperator(InfixOperator("+", noWS, 1, Assoc.Left, (+)))
    opp.AddOperator(InfixOperator("*", noWS, 2, Assoc.Left, (*)))
    opp.AddOperator(InfixOperator("/", noWS, 2, Assoc.Left, (/)))
    
    let expr = opp.ExpressionParser
    let term = pint32 <|> between (str "(") (str ")") expr
    opp.TermParser <- term
    
    let parse str = run expr str