
- •Университет наяновой
- •М. А. Шамашов основные структуры данных и алгоритмы компиляции
- •Предисловие
- •Введение
- •1. Краткий обзор процесса компиляции
- •2. Лексический анализ
- •0123...9 Пробел
- •3. Организация таблиц компилятора
- •3.1. Общий вид таблиц
- •3.2. Прямой доступ к таблице или метод индексов
- •3.3. Неупорядоченная таблица или метод линейного списка
- •3.4. Упорядоченная таблица. Бинарный, двоичный или логарифмический поиск
- •3.5. Сбалансированные деревья
- •3.6. Деревья оптимального поиска
- •3.7.1. Рехеширование
- •3.7.3. Метод цепочек или гроздей
- •4. Общие методы синтаксического анализа
- •4.1. Нисходящий разбор с возвратами
- •4.2. Восходящий разбор с возвратами
- •4.3. Символьный препроцессор на основе бэктрекинга
- •4.3.1. Фаза анализа и перевода грамматики во внутреннее представление
- •4.3.2. Лексичекий анализ в сп
- •4.3.3. Синтаксический анализ в сп
- •4.3.4. Выполнение семантических действий
- •5. Однопроходный синтаксический анализ без возвратов
- •5.1. Ll(k) языки и грамматики
- •5.1.1. Предсказывающие алгоритмы разбора и разбор для ll(1)-грамматик
- •5.1.2. Рекурсивный спуск
- •5.2. Языки и грамматики простого предшествования
- •Xy, если u xy
- •X y, если u xU1) (y l(u1))
- •X y, если (u u1y) (X r(u1)) or
- •5.2.1. Алгоритм Вирта–Вебера для анализа языков простого предшествования
- •5.2.2. Функции предшествования.
- •5.2.3. Проблемы построения грамматик предшествования
- •5.3. Операторная грамматика предшествования
- •6. Введение в семантику
- •6.1. Внутренние формы исходной программы
- •6.1.1. Польская инверсная запись
- •If выр then инстр 1 else инстр 2
- •6.1.2. Интерпретация полиЗа
- •6.1.3. Генерирование команд по полиЗу
- •6.1.4. Тетрады и триады
- •6.2. Семантические подпрограммы перевода инфиксной записи в полиз и аспекты их реализации
- •6.3. Семантические подпрограммы для перевода в тетрады
- •6.4. Метод замельсона–бауэра для перевода в полиз и тетрады
- •6.5. Нейтрализация ошибок
- •6.5.1. Исправления орфографических ошибок
- •6.5.2. Нейтрализация семантических ошибок
- •6.5.3. Нейтрализация синтаксических ошибок
- •7. Машинно-независимая оптимизация программ
- •7.1. Исключение общих подвыражений
- •7.2. Вычисления во время компиляции
- •7.3. Оптимизация булевых выражений
- •7.4. Вынесение инвариантных вычислений за цикл
- •8. Машинно-зависимые фазы компиляции
- •8.1. Распределение памяти
- •8.2. Генерация кода и сборка
- •8.3. Трансляция с языка ассемблера
- •Заключение
- •Список литературы
- •Содержание
- •1. Краткий обзор процесса компиляции 5
- •2. Лексический анализ 10
- •3. Организация таблиц компилятора 16
- •4. Общие методы синтаксического анализа 28
- •5. Однопроходный синтаксический анализ без возвратов 52
- •6. Введение в семантику 78
- •7. Машинно-независимая оптимизация программ 102
- •8. Машинно-зависимые фазы компиляции 109
3.7.3. Метод цепочек или гроздей
В методе цепочек используется одна постоянная по размеру хеш–таблица, содержащая указатели на головы списков слов, вначале указывающих на пустые элементы.
Поиск поступившего слова и его добавление к списку, в случае необходимости, осуществляется за 5 шагов следующим образом:
1. По входному слову определяется хеш–функция или индекс. Этот индекс определяет номер элемента хеш–таблицы, то есть определяет указатель на список слов с полученным индексом. Если этот указатель равен NIL, то есть указывает на пустой список, то выполняется шаг 2, иначе шаг 3.
2. Формируется головной элемент списка слов с данным индексом, в который заносится входное слово. Указатель на сформированный элемент списка заносится в хеш–таблицу на место, определенное индексом слова. Выполняется шаг 5.
3. Найденный элемент хеш–таблицы указывает на некоторый связанный список слов, а именно тех слов, индекс которых совпадает с вычисленным индексом. Поиск в этом списке осуществляется до тех пор, пока не произойдет совпадение входного слова с элементом списка и далее последует шаг 5, или будет достигнут конец списка, и выполнится шаг 4.
4. Множество не содержит входного слова, его нужно добавлять, сформировав новый элемент списка и связав его, например, с последним из рассмотренных элементов. Далее выполняется шаг 5.
5. Алгоритм завершает работу, возвращая указатель на найденный или сформированный элемент.
В литературе неслучайно хеш–методы иногда описываются в терминах “ гроздей “ (buckets). Говорят, что каждый хеш–индекс указывает на некоторую гроздь, и все слова, имеющие этот индекс, принадлежит одной грозди.
Для примера возьмем слова из дерева поиска с рисунка 3.3 и изобразим на рис. 3.9 возможный результат предложенного метода хеширования. Для того чтобы вычислить индекс, в этом примере нужно сложить номера (в алфавитном порядке) двух первых букв слова и взять остаток от деления результата суммирования на 7. Например, индекс слова break получается сложением 2 (номер b) и 18 (номер r) и последующим делением результата (20) на 7. Остаток от деления равен 6. Это значит, что указатель списка, содержащий слово break (если такой список существует), находится в 6–ой ячейке хеш–таблицы.
Реализация предложенной процедуры хеширования зависит от метода вычисления индекса (хеш–функции) и объема хеш–таблицы. Поскольку эти факторы могут оказывать существенное влияние на скорость и затраты памяти процедуры (а фактически и всего компилятора), остановимся на них подробнее.
Единственная цель вычисления индекса – это уменьшение длины списков, в которых должен производится поиск. В идеале нам хотелось бы, чтобы списки были одинаковой длины. К сожалению, разработчик должен выбрать хеш–функцию до того, как он узнает, какие слова появится в ходе компиляции. Поэтому единственное, на что можно рассчитывать, – это то, что новые слова будут попадать в заданные списки с одинаковой вероятностью. Перед разработчиком стоит задача поиска хеш–функции, удовлетворяющей этим свойством псевдослучайности.
Для примера вначале рассмотрим не такую уж случайную хеш–функцию, которая предполагает, что каждое слово относиться к одному из 26 списков по первой букве слова. Так как в английском языке гораздо больше слов, которые начинаются с буквы R, чем слов, начинающихся с буквы О, следует ожидать, что R – список будет содержать гораздо больше элементов, чем О – список. Такая неравномерность еще более усилится, если программирующий на ФОРТРАНе решит, что в его программе все идентификаторы целых переменных будут начинаться с буквы I. Итак, предположенный метод плох всем за исключением способа получения индекса.
Метод индексации по двум буквам, примененный в примере на рис. 3.9, лучше, чем метод индексации по одной букве, так как он ведет к более равномерному заполнению списков. Однако программист может употреблять имена с одинаковым началом (например, alpha1, alpha2, alpha3 и т.д.), что отрицательно повлияет на работу компилятора. Можно добиться быстрого поиска, если вычислять индекс по сумме всех букв. Однако затраты при вычислении индекса могут перекрыть выигрыш во времени поиска. Таким образом, нужно найти компромисс между временем поиска и времени вычисления индекса. На практике используются хеш–функции рассмотренные выше в одноименном разделе.
Определим теперь, насколько большой нужно делать хеш–таблицу. Считая, что слова помещаются в списки случайным образом, можно изучить вопрос об объеме таблицы чисто количественным методом. Пусть в хеш–таблице есть место для С указателей, и пусть случайным образом введены М слов. Если входное слово случайно выбрано из М слов, то ожидаемое число сравнений Е, необходимых для нахождения входного слова, будет равно:
Е = 1 + (М –1) / (2С).
Чтобы установить, что некоторого нового слова в множестве нет, нужно провести M/C сравнений. Если нам известно число слов, с которыми придется иметь дело, эта формула говорит нам, как именно число сравнений зависит от объема таблицы. В таблице 3.1 помещены результаты вычислений для некоторых значений М и С.
Пусть нас интересует эффективность обработки 500 различных слов, и мы хотим узнать сколько списков выгоднее завести: 500 или 100. Преимуществом 100 списков является то, что экономится место, необходимое для 400 указателей, а недостатком – то, что для идентификации слова необходимо (в среднем) на 2 сравнения больше. Решение сводится к выбору между 400 дополнительными указателями и 2 дополнительными сравнениями, необходимыми для идентификации каждого слова. Но этот поиск выполняются по 10 раз для каждой строки программы. Неизбежно напрашивается вывод, что на размере хеш–таблицы экономить не надо. Еще лучшего эффекта можно достигнуть, сочетая хеш–метод цепочек и деревья поиска.
Таблица 3.1.
Число слов, М
|
|
20 |
100 |
500 |
1000 |
5000 |
|
10 |
1.95 |
5.99 |
25.95 |
50.95 |
250.95 |
Объем |
50 |
1.19 |
1.99 |
5.99 |
10.99 |
50.99 |
таблицы, |
100 |
1.10 |
1.50 |
3.50 |
5.60 |
20.00 |
С |
200 |
1.05 |
1.25 |
2.25 |
3.50 |
13.50 |
|
500 |
1.02 |
1.10 |
1.50 |
2.00 |
6.00 |
|
1000 |
1.01 |
1.01 |
1.50 |
1.50 |
3.50 |
В компиляторе ФОРТРАНа (IBM 1966 год), работали, например, с 6 деревьями, по одному дереву для идентификаторов, состоящих из 1, 2, 3, 4, 5 и 6 символов соответственно. Имелось, также, по одному дереву для 4, 8 и 16 битовых констант, дерево для меток инструкций и несколько других. Все элементы содержались в одной и той же таблице, каждый элемент которой занимает 52 байт.
Упражнения на программирование.
3.1. Реализуйте программу, осуществляющую поиск и помещающую идентификаторы исходных текстов (файлов) на произвольных языках программирования в сбалансированное дерево поиска. Предложите алгоритм визуализации, обеспечивающий просмотр формируемых деревьев.
3.2. Напишите программу формирования таблицы идентификаторов фиксированного размера с возможностью выбора способа вычисления хеш–функции и алгоритма рехеширования. Оттестируйте вашу программу с использованием файлов, содержащих исходные модули программ на произвольном языке высокого уровня. Проведите оценку среднего времени поиска информации в построенных таблицах и сопоставьте полученные оценки с теми значениями, которые получаются из формул раздела 3.7.2.
3.3. Напишите программу, реализующую метод гроздей с возможностью визуализации как отдельных списков для заданного значения хеш–функции, так и общего представления для оценки степени заполнения списков, например, в виде гистограммы.
3.4. Реализуйте алгоритм построения оптимального дерева поиска для множества ключевых слов и служебных символов языка программирования высокого уровня, на котором Вы работаете.