Можно ли сделать комбинаторы парсера эффективными? - PullRequest
33 голосов
/ 30 декабря 2010

Около 6 лет назад я провел сравнительный анализ своих собственных комбинаторов синтаксического анализа в OCaml и обнаружил, что они были примерно в 5 раз медленнее, чем предлагаемые в данный момент генераторы синтаксического анализатора. Недавно я вернулся к этой теме и протестировал Parsec на Haskell против простого обработанного вручную анализатора восхождений , написанного на F #, и с удивлением обнаружил, что F # в 25 раз быстрее, чем Haskell.

Вот код на 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

У меня сложилось впечатление, что даже самые современные комбинаторы парсеров тратят много времени на отслеживание. Это верно? Если да, возможно ли написать комбинаторы синтаксического анализатора, которые генерируют конечные автоматы для достижения конкурентоспособной производительности, или необходимо использовать генерацию кода?

EDIT:

Вот скрипт OCaml, который я использовал для генерации выражения ~ 2Mb для бенчмаркинга:

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 ]

59 голосов
/ 30 декабря 2010

Я предложил решение на Haskell, которое в 30 раз быстрее, чем решение на Haskell, которое вы опубликовали (с моим придуманным тестовым выражением).

Основные изменения:

  1. Изменить Parsec / String на Attoparsec / ByteString
  2. В функции fact измените read & many1 digit на decimal
  3. Сделан строгий рекурсия 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)

Полагаю, из этого я заключил, что большая часть замедления для комбинатора синтаксического анализа заключалась в том, что он находился на неэффективной основе, а не в том, что это был комбинатор синтаксического анализатора как таковой.

Я предполагаю, что с большим количеством времени и профилирования это могло бы пойти быстрее, поскольку я остановился, когда прошел отметку 25 ×.

Я не знаю, будет ли это быстрее, чем анализатор восхождений по приоритетам, портированный на Haskell. Может быть, это будет интересный тест?

32 голосов
/ 31 декабря 2010

В настоящее время я работаю над следующей версией FParsec (v. 0.9), которая во многих ситуациях улучшит производительность почти в 2 раза по сравнению с текущей версией .

[Обновление: FParsec 0.9 был выпущен, см. http://www.quanttec.com/fparsec]

Я протестировал реализацию синтаксического анализатора F # Джона против двух реализаций FParsec. Первый парсер FParsec является прямым переводом парсера djahandarie. Второй использует встроенный компонент приоритета оператора FParsec. В качестве входных данных я использовал строку, сгенерированную сценарием Джона OCaml с параметром 10, что дает мне размер ввода около 2,66 МБ. Все парсеры были скомпилированы в режиме выпуска и работали на 32-битной .NET 4 CLR. Я измерял только чистое время анализа и не включал время запуска или время, необходимое для построения входной строки (для синтаксических анализаторов FParsec) или списка символов (синтаксический анализатор Джона).

Я измерил следующие числа (обновленные числа для v. 0,9 в паранах):

  • Ручной анализатор Джона: ~ 230 мс
  • Парсер FParsec # 1: ~ 270 мс (~ 235 мс)
  • Парсер FParsec # 2: ~ 110 мс (~ 102 мс)

В свете этих цифр я бы сказал, что комбинаторы синтаксического анализа могут определенно предлагать конкурентоспособную производительность, по крайней мере, для этой конкретной проблемы, особенно если принять во внимание, что FParsec

  • автоматически генерирует легко читаемые сообщения об ошибках,
  • поддерживает очень большие файлы в качестве входных данных (с произвольным возвратом) и
  • поставляется с декларативным, настраиваемым модулем синтаксического анализа приоритета оператора.

Вот код для двух реализаций FParsec:

Parser # 1 ( Перевод парсера Джахандари ):

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
13 голосов
/ 30 декабря 2010

Короче говоря, комбинаторы парсера медленны для лексинга.

Была библиотека комбинаторов Haskell для построения лексеров (см. «Ленивое лексирование - это быстро», Мануэль М.Т. Чакраварти) - поскольку таблицы генерировались во время выполнения, не возникало проблем с генерацией кода.Библиотека немного привыкла - она ​​изначально использовалась в одном из препроцессоров FFI, но я не думаю, что она когда-либо загружалась в Hackage, так что, возможно, это было слишком неудобно для обычного использования.

В приведенном выше коде OCaml синтаксический анализатор напрямую сопоставляется в списках символов, поэтому он может быть настолько быстрым, насколько деструктуризация списка происходит на языке хоста (он будет намного быстрее, чем Parsec, если он будет повторно реализован вHaskell).Кристиан Линдиг имел библиотеку OCaml, которая имела набор комбинаторов синтаксического анализа и набор комбинаторов лексеров - комбинаторы лексеров были, конечно, намного проще, чем мануэль Чакраварти, и, возможно, стоило бы отследить эту библиотеку и провести ее тестирование перед написанием лексерагенератор.

8 голосов
/ 30 декабря 2010

Вы пробовали одну из известных библиотек быстрого парсера?Целью Parsec никогда не была скорость, а простота использования и ясность.Сравнение с чем-то вроде attoparsec может быть более справедливым сравнением, особенно потому что строковые типы, вероятно, будут более равными (ByteString вместо String).

Мне также интересно, какие флаги компиляции использовались.Это еще один троллинг-пост печально известного Джона Харропа, меня не удивит, если вообще не будет использована оптимизация для кода на Haskell.

...