Эффективность парсера уравнений

Я потонул около месяца полного времени в собственный синтаксический анализатор С++. Он работает, за исключением того, что он медленный (в 30-100 раз медленнее, чем жестко закодированное уравнение). Что я могу изменить, чтобы сделать это быстрее?

Я прочитал все, что мог найти на эффективном коде. В широких штрихах:

  • Парсер преобразует выражение строкового уравнения в список объектов "операция".
  • Объект операции имеет два указателя на функции: "getSource" и "оценить".
  • Чтобы оценить уравнение, все, что я делаю, это цикл for в списке операций, вызывающий каждую функцию по очереди.

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

  • Я пробовал встраивать все функции, на которые указывает указатель на функцию - никаких улучшений.
  • Помогло ли переключение с указателей функций на функторов?
  • Как насчет удаления фрейма указателя на функцию и вместо этого создается полный набор производных классов "операций", каждый со своими собственными виртуальными функциями "getSource" и "оценка"? (Но разве это не просто перемещает указатели на функции в таблицу vtable?)

У меня много кода. Не уверен, что делать. Попросите какой-то аспект этого, и вы получите.

Ответ 1

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

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

Интерпретация - другое дело. Дифференциал скорости между интерпретируемым и скомпилированным кодом обычно в 10-100 раз медленнее, если только основные операции не являются длительными. Тем не менее, вы все еще можете его оптимизировать.

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

Всякий раз, когда я делаю то, что вы делаете, то есть предоставляете язык пользователю, но я хочу, чтобы язык имел быстрое выполнение, что я делаю: Я переводил исходный язык в язык, на котором у меня есть компилятор, а затем скомпилируйте его на лету в .dll(или .exe) и запустите его. Это очень быстро, и мне не нужно писать переводчика или беспокоиться о том, насколько это быстро.

Ответ 2

В своем сообщении вы не упоминаете, что вы профилировали код. Это первое, что я сделал бы, если бы был на твоем месте. Это даст вам хорошее представление о том, где потрачено время и где сосредоточить усилия по оптимизации.

Ответ 3

Самое первое: профайл, что на самом деле пошло не так. Является узким местом при анализе или оценке? valgrind предлагает некоторые инструменты, которые могут вам помочь.

Если он разбирается, boost:: spirit может вам помочь. Если в оценке, помните, что виртуальные функции могут быть довольно медленными для оценки. Я сделал довольно хороший опыт с рекурсивным boost:: вариантом.

Ответ 4

Вы знаете, что создание рекурсивного парсера с выражением выражения очень просто, грамматика LL (1) для выражений - это всего лишь пара правил. Затем анализ становится линейным делом, и все остальное может работать над деревом выражений (при синтаксическом анализе); вы должны собирать данные из нижних узлов и передавать их на более высокие узлы для агрегации.

Это позволит избежать указания указателей функций/классов для определения пути вызова во время выполнения, полагаясь вместо проверенной рекурсии (или вы можете создать итеративный парсер LL, если хотите).

Ответ 5

Кажется, что вы используете довольно сложную структуру данных (как я ее понимаю, синтаксическое дерево с указателями и т.д.). Таким образом, переход через разыменование указателя не очень эффективен по памяти (множество случайных доступов) и может значительно замедлить вас. Как предложил Майк Данлави, вы можете скомпилировать все выражение во время выполнения с использованием другого языка или встраивания компилятора (например, LLVM). Для того, что я знаю, Microsoft.Net предоставляет эту функцию (динамическую компиляцию) с деревьями Reflection.Emit и Linq.Expression.

Ответ 6

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

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

Почти единственное, что обычно дает лучшую производительность, чем это, нужно делать, как советуют @Mike Dunlavey, и просто генерировать исходный код и запускать его через "настоящий" компилятор. Это, однако, довольно "тяжелое" решение. Если вам действительно нужна максимальная скорость, это, безусловно, лучшее решение, но если вы просто хотите улучшить то, что делаете сейчас, конвертация в RPN и интерпретация, которая обычно дает довольно приличное улучшение скорости для небольшого количества кода.