Haskell 解析器组合器能变得高效吗?
大约6年前,我在OCaml中对自己的解析器组合器进行了基准测试,发现它们比当时提供的解析器生成器慢约5倍。我最近重新探讨了这一主题,并对Haskell的Parsec与用F#编写的简单手卷进行了基准测试,惊讶地发现F#比Haskell快25倍 下面是我用来从文件中读取大型数学表达式、解析和计算的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 ((+)
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倍 主要变化:
事实
功能中,将读取
和多个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
设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