Рекурсивный Descent Parser для чего-то простого? - PullRequest
6 голосов
/ 03 апреля 2011

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

Вскоре после этого я сравнил производительность всех моих предыдущих методов анализа. Парсер рекурсивного спуска был на сегодняшний день самым медленным. Я застрял: стоит ли использовать синтаксический анализатор с рекурсивным спуском для чего-то простого, или я оправдан в выборе ярлыков? Я хотел бы пойти по пути чистого регулярного выражения, который безумно быстр (почти в 3 раза быстрее, чем анализатор RD), но очень хакерский и до некоторой степени не поддерживаемый. Я полагаю, что производительность не очень важна, потому что скомпилированные шаблоны кэшируются, но является ли анализатор рекурсивного спуска подходящим инструментом для каждой задачи? Я полагаю, что мой вопрос можно рассматривать скорее как философский: в какой степени стоит жертвовать ремонтопригодностью / гибкостью для производительности?

Ответы [ 4 ]

6 голосов
/ 05 апреля 2011

Парсеры рекурсивного спуска могут быть чрезвычайно быстрыми.

Они обычно организованы с помощью лексера, который использует регулярные выражения для распознавания жетонов языка, которые передаются парсеру.Большая часть работы по обработке исходного текста выполняется за символом с помощью лексера с использованием безумно быстрых FSA, в которые часто компилируются RE.

Парсер видит токены только изредка по сравнению со скоростью, с которой лексер видит символы, поэтому его скорость часто не имеет значения.Однако при сравнении скоростей анализатора и анализатора, игнорируя время, необходимое для преобразования токенов, анализаторы рекурсивного спуска могут быть очень быстрыми, поскольку они реализуют стек синтаксического анализатора с использованием вызовов функций, которые уже очень эффективны по сравнению с обычным синтаксическим анализатором push-current-state-на имитируемом стеке.

Итак, вы можете съесть и торт.Используйте регулярные выражения для лексем.Используйте синтаксический анализатор (любой вид, рекурсивный спуск просто прекрасен) для обработки лексем.Вы должны быть довольны производительностью.

Этот подход также удовлетворяет наблюдения, сделанные другими ответами: напишите его таким образом, чтобы сделать его ремонтопригодным.Разделение Lexer / Parser делает это очень хорошо, уверяю вас.

0 голосов
/ 30 декабря 2017

Парсер рекурсивного спуска должен быть быстрее

... или вы делаете что-то не так.

Во-первых, ваш код должен быть разбит на 2 отдельныхшаги.Лексер + Парсер.

Некоторые справочные примеры в Интернете сначала разбивают весь синтаксис на большие промежуточные структуры данных, а затем передают его синтаксическому анализатору.Хотя хорошо для демонстрации;не делайте этого, это удваивает время и сложность памяти.Вместо этого, как только лексер определит совпадение, уведомите синтаксический анализатор о переходе состояния или переходе состояния + данные.

Что касается лексера.Это, вероятно, где вы найдете свое текущее узкое место.Если лексер четко отделен от вашего парсера, вы можете попробовать переключаться между реализациями Regex и не-Regex, чтобы сравнить производительность.

Regex ни в коем случае не быстрее, чем чтение необработанных строк.Это просто позволяет избежать некоторых распространенных ошибок по умолчанию.В частности, ненужное создание строковых объектов.В идеале, ваш лексер должен сканировать ваш код и производить вывод с нулевыми промежуточными данными, кроме минимума, необходимого для отслеживания состояния в вашем анализаторе.В отношении памяти у вас должно быть:

  • Необработанный ввод (т. Е. Источник)
  • Состояние анализатора (например, isExpression, isSatement, строка, столбец)
  • Данные (например, AST, Tree, 2D Array и т. Д.).

Например, если ваш текущий лексер соответствует нетерминалу и копирует каждый символ по одному, пока не достигнет следующего терминала;по сути, вы воссоздаете эту строку для каждой подходящей буквы.Имейте в виду, что строковые типы данных неизменны, concat всегда будет создавать новую строку.Вы должны сканировать текст, используя арифметику указателя или какой-либо эквивалент.

Чтобы решить эту проблему, вам нужно отсканировать от начальных позиций нетерминала до конца нетерминала и копировать только при совпадениизавершено.

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

Ключ в том, что не пытайтесь использовать Regex в качестве конечного автомата.В лучшем случае он будет работать только для декларативных синтаксисов Regular (то есть Chomsky Type III, без стека) - отсюда и название Regular Expression.Например, HTML является декларативным синтаксисом без контекста (т. Е. Chomsky Type II, основанный на стеке), поэтому одного Rexeg никогда не достаточно для его анализа.Ваша грамматика и вообще все синтаксисы шаблонов попадают в эту категорию.Вы уже достигли предела Regex, поэтому вы на правильном пути.

Используйте Regex только для токенизации.Если вы действительно обеспокоены производительностью, перепишите свой лексер, чтобы исключить все ненужное копирование строк и / или промежуточные данные.Посмотрите, сможете ли вы превзойти версию Regex.

Ключ к тому же.Версию Regex легче понять и поддерживать, в то время как ваш лексер, созданный вручную, скорее всего, будет быстрее, если будет написан правильно.Обычная мудрость гласит: сделай себе одолжение и отдай предпочтение первому.С точки зрения сложности Big-O, между ними не должно быть никакой разницы.Это две формы одного и того же.

0 голосов
/ 06 апреля 2011

В какой степени стоит жертвовать ремонтопригодностью / гибкостью ради производительности?

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

Также важно не изобретать колесо.Комментарий о взгляде на другой парсер очень хорош.Часто встречаются общие решения для написания таких подпрограмм, как этот.

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

0 голосов
/ 03 апреля 2011

Сначала читаемость, потом производительность ...

Так что, если ваш парсер делает код более читабельным, тогда это правильный инструмент

...