Czy kombinatory parserów mogą być wydajne?
Około 6 lat temu, porównałem moje własne kombinatory parserów w OCaml i odkryłem, że były ~5× wolniejsze niż Generatory parserów w ofercie w tym czasie. Niedawno zrewidowałem ten temat i porównałem Parsec Haskella z prostym ręcznie zwijanym parserem , napisanym w F# i byłem zaskoczony, że F # jest 25× szybszy od Haskella.
Oto Kod Haskella, którego użyłem do odczytania dużego wyrażenia matematycznego z pliku, analizy i oceny it:
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"
A oto mój samodzielny parser wspinaczkowy W 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
Mam wrażenie, że nawet najnowocześniejsze kombinatory parserów tracą dużo czasu na śledzenie wstecz. Zgadza się? Jeśli tak, to czy możliwe jest pisanie kombinatorów parserów, które generują maszyny stanowe w celu uzyskania konkurencyjnej wydajności, czy też konieczne jest użycie generowania kodu?
EDIT:
Oto skrypt OCaml, którego użyłem do wygenerowania wyrażenia ~2MB dla "benchmarking": {]}
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
4 answers
Obecnie pracuję nad kolejną wersją FParsec (V.0. 9), która w wielu sytuacjach poprawi wydajność nawet o współczynnik 2 w stosunku do aktualnej wersji.
[Aktualizacja: fparsec 0.9 został wydany, zobacz http://www.quanttec.com/fparsec ]
Przetestowałem implementację F # parsera Jona pod kątem dwóch implementacji FParsec. Pierwszy parser FParsec jest bezpośrednim tłumaczeniem parsera djahandarie. Drugi wykorzystuje wbudowany FParsec komponent pierwszeństwa operatora. Jako wejście użyłem napisu wygenerowanego skryptem OCaml Jona z parametrem 10, który daje mi wielkość wejścia około 2,66 MB. Wszystkie parsery zostały skompilowane w trybie release i były uruchamiane na 32-bitowym CLR.NET 4. Zmierzyłem tylko czysty czas parsowania i nie uwzględniłem czasu uruchamiania ani czasu potrzebnego do zbudowania łańcucha wejściowego (dla parserów FParsec) lub listy znaków (parser Jona).
Zmierzyłem następujące numery (zaktualizowane numery dla V. 0. 9 w parens): {]}
- ręczny Parser Jona: ~230ms
- fparsec parser # 1: ~270ms (~235ms)
- fparsec parser # 2: ~110ms (~102ms)
W świetle tych liczb, powiedziałbym, że kombinatory parserów mogą zdecydowanie oferować konkurencyjną wydajność, przynajmniej dla tego konkretnego problemu, zwłaszcza jeśli wziąć pod uwagę, że FParsec
- automatycznie generuje bardzo czytelne komunikaty o błędach,
- obsługuje bardzo duże pliki jako wejście (z dowolny backtracking), oraz
- jest dostarczany z deklaratywnym, konfigurowalnym modułem parsera pierwszeństwa operatora.
Oto kod dla dwóch implementacji FParsec:
Parser #1 (tłumaczenie parsera 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 (implementacja Idiomatic 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
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-04-27 18:43:10
Wymyśliłem rozwiązanie Haskella, które jest 30× szybsze niż rozwiązanie Haskella, które napisałeś (z moim wymyślonym wyrażeniem testowym).
Główne zmiany:
- Zmień Parsec / String Na Attoparsec / ByteString
- w funkcji
fact
Zmieńread
&many1 digit
dodecimal
- Wykonane
chainl1
rekurencja ścisła (Usuń $! dla wersji lazier).
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)
I guess what I concluded z tego wynika, że większość spowolnienia dla kombinatora parserów polegała na tym, że siedział on na nieefektywnej podstawie, a nie że był kombinatorem parserów, per se.
Wyobrażam sobie, że z większą ilością czasu i profilowaniem może to pójść szybciej, ponieważ zatrzymałem się, gdy przeszedłem znak 25×.
Nie wiem, czy byłoby to szybsze niż Parser wspinaczkowy przeniesiony do Haskell. Może to byłby interesujący test?
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-12-30 06:51:35
W skrócie, kombinatory parserów są wolne dla lexingu.
Była biblioteka Haskell combinator do budowania lexerów ( zobacz" Lazy Lexing is Fast " Manuel M. T. Chakravarty) - ponieważ tabele były generowane w czasie wykonywania, nie było kłopotów z generowaniem kodu. Biblioteka trochę się zużyła - początkowo była używana w jednym z preprocesorów FFI, ale nie sądzę, aby kiedykolwiek została załadowana do Hackage ' a, więc może była trochę zbyt niewygodna dla zwykłego użytkowania.
W kodzie OCaml powyżej, parser jest bezpośrednio dopasowywany na listach znaków, więc może być tak szybki, jak destrukcja listy jest w języku hosta (byłby znacznie szybszy niż Parsec, gdyby został ponownie zaimplementowany w Haskell). Christian Lindig miał bibliotekę OCaml, która miała zestaw kombinatorów parsera i zestaw kombinatorów lexer - kombinatory lexer były z pewnością znacznie prostsze niż Manuel Chakravarty ' s, i może warto byłoby śledzić tę bibliotekę i bench-znakowanie go przed napisaniem lexer generator.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-12-30 08:35:57
Czy próbowałeś jednej ze znanych bibliotek szybkiego parsera? Celem Parsec nigdy tak naprawdę nie była szybkość, ale łatwość użycia i przejrzystość. Porównywanie z czymś takim jak attoparsec może być bardziej sprawiedliwym porównaniem, zwłaszcza, że typy łańcuchów mogą być bardziej równe (ByteString
zamiast String
).
Zastanawiam się również, które flagi kompilacji zostały użyte. Jest to kolejny trollingowy post autorstwa niesławnego Jona Harropa, nie zdziwiłoby mnie, gdyby w ogóle nie użyto optymalizacji dla Haskella kod.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-05-29 16:44:05