Эффективность парсера уравнений - PullRequest
11 голосов
/ 20 сентября 2011

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

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

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

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

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

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

Ответы [ 6 ]

5 голосов
/ 20 сентября 2011

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

1 голос
/ 20 сентября 2011

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

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

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

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

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

0 голосов
/ 20 сентября 2011

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

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

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

0 голосов
/ 20 сентября 2011

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

0 голосов
/ 20 сентября 2011

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

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

0 голосов
/ 20 сентября 2011

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

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

...