大约6年前,我在OCaml中对自己的解析器组合器进行了基准测试,发现它们比当时提供的解析器生成器慢〜5倍。我最近重新审视了这个主题,并对Haskell的Parsec与使用F#编写的简单手工precedence climbing parser进行了对比,并惊讶地发现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"
这是我在F#中的自包含优先级攀爬解析器:
let rec (|Expr|) = function
| P(f, xs) -> Expr(loop (' ', f, xs))
| xs -> invalidArg "Expr" (sprintf "%A" xs)
and loop = function
| ' ' as oop, f, ('+' | '-' as op)::P(g, xs)
| (' ' | '+' | '-' as oop), f, ('*' | '/' as op)::P(g, xs) ->
let h, xs = loop (op, g, xs)
match op with
| '+' -> (+) | '-' -> (-) | '*' -> (*) | '/' | _ -> (/)
|> fun op -> loop (oop, op f h, xs)
| _, f, xs -> f, xs
and (|P|_|) = function
| '('::Expr(f, ')'::xs) -> Some(P(f, xs))
| c::_ as xs when '0' <= c && c <= '9' ->
let rec loop n = function
| c2::xs when '0' <= c2 && c2 <= '9' -> loop (10*n + int(string c2)) xs
| xs -> Some(P(n, xs))
loop 0 xs
| _ -> None
我的印象是,即使是最先进的解析器组合器也会浪费大量时间来追踪。那是对的吗?如果是这样,是否可以编写生成状态机以获得竞争性能的解析器组合器,或者是否有必要使用代码生成?
修改
这是我用来为基准测试生成~2Mb表达式的OCaml脚本:
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
答案 0 :(得分:59)
我提出了一个Haskell解决方案,比你发布的Haskell解决方案快30倍(使用我编制的测试表达式)。
主要变化:
fact
功能中,更改read
&amp; many1 digit
至decimal
chainl1
递归(删除$!for lazier version)。我尽量保持你所拥有的一切尽可能相似。
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)
我想我从中得出的结论是,解析器组合器的大部分减速是因为它本身就是一个低效的基础,而不是它本身是一个解析器组合器。
我想有更多的时间和分析,这可能会更快,因为当我超过25×标记时我停止了。
我不知道这是否会比移植到Haskell的优先攀爬解析器更快。也许这将是一个有趣的测试?
答案 1 :(得分:32)
我目前正在研究FParsec的下一个版本(v.9。),在许多情况下,相对于current version,性能最多会提高2倍。
[更新:FParsec 0.9已发布,请参阅http://www.quanttec.com/fparsec]
我已经针对两个FParsec实现测试了Jon的F#解析器实现。第一个FParsec解析器是djahandarie解析器的直接翻译。第二个使用FParsec的可嵌入运算符优先级组件。作为输入,我使用了带有参数10的Jon的OCaml脚本生成的字符串,这给了我大约2.66MB的输入大小。所有解析器都在发布模式下编译,并在32位.NET 4 CLR上运行。我只测量了纯解析时间,并且没有包括启动时间或构造输入字符串(对于FParsec解析器)或char列表(Jon的解析器)所需的时间。
我测量了以下数字(pare 0.9的更新数字):
根据这些数字,我会说解析器组合器肯定能提供有竞争力的性能,至少对于这个特定的问题,特别是如果你考虑到FParsec
这是两个FParsec实现的代码:
解析器#1( djahandarie解析器的翻译):
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
Parser#2(惯用语FParsec实现):
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
答案 2 :(得分:13)
简而言之,解析器组合器很慢。
有一个用于构建词法分析器的Haskell组合器库(请参阅“Lazy Lexing is Fast”Manuel M. T. Chakravarty) - 因为这些表是在运行时生成的,所以没有代码生成的麻烦。该库得到了一些使用 - 它最初用于其中一个FFI预处理器,但我认为它没有上传到Hackage,所以也许它对于常规使用来说有点太不方便了。
在上面的OCaml代码中,解析器直接匹配char-list,因此它可以像使用宿主语言的列表解构一样快(如果它在Haskell中重新实现,它将比Parsec快得多)。 Christian Lindig有一个OCaml库,它有一组解析器组合器和一组词法器组合器 - 词法分析器组合器肯定比Manuel Chakravarty简单得多,并且可能值得追踪这个库并在编写词法分析器之前对其进行基准测试发生器。
答案 3 :(得分:8)
您是否尝试过一种已知的快速解析器库? Parsec的目标从来没有真正的速度,但易用性和清晰度。比较像attoparsec这样的东西可能是一个更公平的比较,特别是因为字符串类型可能更平等(ByteString
而不是String
)。
我也想知道使用了哪些编译标志。这是臭名昭着的Jon Harrop的另一个拖钓帖子,如果没有对Haskell代码使用任何优化,我不会感到惊讶。