Подтвердить что ты не робот

Могут ли компиляторы анализатора быть эффективными?

Примерно 6 лет назад я сравнивал свои собственные синтаксические анализаторы в OCaml и обнаружил, что они были ~ 5 × медленнее, чем генераторы парсеров, предлагаемые в то время. Недавно я пересматривал этот вопрос и сравнивал Haskell Parsec с простым ручным каталогом

и здесь мой автономный стартовый парсер в 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 script Я использовал для генерации выражения ~ 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
4b9b3361

Ответ 1

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

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

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

Я измерил следующие числа (обновленные числа для v. 0.9 в parens):

  • Джон с ручным парсером: ~ 230 мс
  • Парсер FParseС# 1: ~ 270 мс (~ 235 мс)
  • Парсер FParseС# 2: ~ 110 мс (~ 102 мс)

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

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

Здесь код для двух реализаций FParsec:

Parser # 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

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

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

  • Изменить Parsec/String на Attoparsec/ByteString
  • В функции fact измените read и many1 digit на decimal
  • Сделал рекурсию 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. Может быть, это будет интересный тест?

Ответ 3

В двух словах комбинаторы парсеров медленны для лексинга.

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

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

Ответ 4

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

Я также задаюсь вопросом, какие компиляционные флаги использовались. Это еще один троллинговый пост от пресловутого Джона Харропа, это не удивило бы меня, если бы вообще никаких оптимизаций для кода Haskell не было.