- •Университет наяновой
- •М. А. Шамашов основные структуры данных и алгоритмы компиляции
- •Предисловие
- •Введение
- •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.1. Рехеширование
Предположим, что мы хешируем слово S и обнаруживаем, что другое слово уже заняло элемент h. Возникает коллизия. Тогда сравниваем S с элементом h+P1 (по модулю N, где N – длина таблицы) для некоторого целого P1. Если снова возникает коллизия, сравниваем S с элементом h+P2 и т.д. Это продолжается до тех пор, пока не встретится какой-либо элемент h+Pi (по модулю N), который либо пуст, либо содержит S, либо снова является элементом h (Pi=0). В последнем случае мы прекращаем выполнение программы, поскольку таблица полна.
Таким образом, если возникло i коллизий, будет выполнено i+1 сравнений с элементами hi=h+Pi (по модулю N). Величины Pi должны выбираться так, чтобы ожидаемое число сравнений Е было невелико и чтобы по возможности было рассмотрено большее число элементов.
Рехеширование обычно связывается с термином рассеянной памяти, так как заполненные элементы таблицы оказываются рассеянными по ней. Чтобы отличать пустые элементы от заполненных, все элементы должны быть первоначально заполнены каким–либо значением, которое не может встречаться как символ (слово). Кроме того, таблица должна быть сразу рассчитана на максимальное число элементов. Это объясняется тем, что нет простого способа расширения таблицы (массива), если она заполнена, без повторного вычисления хеш–адресов для всех записанных элементов и занесения их в соответствующие новые позиции. Имеются несколько способов рехеширования, которые и будут рассмотрены ниже.
Линейное рехеширование – старейший и, вероятно, наименее эффективный из них. Он состоит в том, чтобы положить P1=1, P2=2, P3=3 и т. д. То есть сравниваются последовательные элементы. Предположим, например, что символы S1 и S2 были хешированны и записаны в элементы 2 и 4 соответственно (см. рис. 3.8 а)
Теперь предположим, что символ S3 также ссылается на элемент 2. Вследствие коллизии он будет занесен в элемент 3 (рис. 3.8 б). Наконец, предположим, что S4 также ссылается на элемент 2. Возникают последовательно 3 коллизии – с S1, S3 и S2 – прежде чем S4 заносится в элемент 5 (рис. 3.8 в). Причина низкой эффективности этого метода становится достаточно ясной из этого примера; после нескольких коллизий, разрешенных таким образом, элементы скапливаются вместе, образуя длинные цепочки заполненных элементов.
Оценка среднего числа сравнений Е для поиска одного элемента, полученная эмпирическим путем, составляет:
Е = (1 – Lf / 2) / (1 – Lf),
где Lf – коэффициент загрузки. Таким образом, если таблица заполнена на 10% мы можем ожидать 1.06 сравнений, если наполовину – 1.5 сравнений, если на 90%, то – 5.5 сравнений. Заметим, что Е не зависит от размера таблицы, а только от степени заполнения.
Случайное рехеширование снимает проблему скопления за счет выбора в качестве Pi псевдослучайных чисел. Если размер таблицы представляется степенью двойки (N = 2k, для произвольного k), то хорошие результаты дает следующий способ вычислений Pi :
1. При вызове программы положить целое R, равным 1.
2. Вычислить каждое Pi следующим образом
а) установить R=R5;
b) выделить младшие k+2 разряда R, и поместить результат в R;
с) взять величину из R, сдвинуть ее вправо на 2 разряда и результат назвать Pi.
Важнейшее свойства этого метода, предотвращающего скопление, состоит в том, что все числа Pi+k – Pi различны. Хорошее приближение ожидаемого числа сравнений в этом случае дает формула:
E = – (1 / Lf) log (1–Lf),
где Lf – коэффициент загрузки. Так, если таблица заполнена на 10% ожидается 1.05 сравнений, если наполовину – то 1.39, если на 90% – 2.56.
Рехеширование сложением
Положим Pi=ih, где h – искомый хеш–индекс. Таким образом мы будем пробовать элементы h, 2h, 3h, 4h и т.д.(все значения вычисляются по модулю, равному размеру таблицы). Этот метод хорош, когда размер таблицы N является простым числом, так как все последовательности накрывают полностью все возможные индексы от 1 до N–1.
Очевидный недостаток рассмотренных методов рехеширования – неизменный, рассчитанный на максимум размер таблицы и это скорее не руководство к действию, а экскурс в историю, хотя подобные подходы и до сих пор практикуются при размещении информации на дисках.
3.7.2. Хеш–функция
Как уже отмечалось, выбор хеш–функции очень сильно влияет на эффективность рассматриваемых методов организации таблиц. Обычно, если слово S, являющееся аргументом хеширования, занимает более одного машинного слова, то на первом шаге хеширования по S формируется одно машинное слово S /. Как правило, S / вычисляется суммированием всех слов из S при помощи поразрядного сложения по модулю 2 (то есть при помощи операция XOR – исключающее или).
На втором шаге из S / вычисляется окончательный индекс, причем это можно сделать несколькими способами:
1. Умножить S / само на себя и использовать n средних битов в качестве значения функции хеширования (если таблица имеет 2n элементов). Поскольку n средних битов зависит от каждого бита S /, этот метод дает очень хорошие результаты.
2. Использовать какую–либо логическую операцию, например тот же XOR, над некоторыми частями S /.
3. Если в таблице имеется 2n элементов, расщепить S / на n частей и просуммировать их. Использовать n правых крайних битов результата.
4. Разделить S / на длину таблицы и остаток использовать в качестве хеш–индекса.
Все эти методы применялись и давали удовлетворительные результаты. Можно предложить и другие методы. Нужно только быть уверенным, что на множестве аргументов, к которому будет применяться функция хеширования, она даст достаточно случайные адреса. Это легко проверить, формируя таким образом таблицу терминалов (ключевых, зарезервированных слов языка). В принципе, выделение ключевых слов в отдельную таблицу вовсе необязательно. Их можно поместить в общую таблицу идентификаторов при начальной загрузке компилятора, снабдив соответствующим признаком. Стоит проверить эту первоначальную таблицу на число возникших коллизий. Хеш–функция может быть достаточно хорошей с точки зрения статистики, но может как раз случиться, что 20 зарезервированных слов ссылается на один и тот же адрес.
Например, в компилятора PL/1 F фирмы IBM, реализованного в начале 70–х годов, использовалось следующая хеш–функция:
1. Суммировались последовательные части идентификатора, содержащие по 4 символа, в один 4–байтовый регистр.
2. Результат делился на 211 и определялся остаток R.
3. Значение 2R использовалось в качестве индекса для ссылки на таблицу из 211 указателей (каждый указателей имеет длину 2 байта).
В этом компиляторе для разрешения коллизии использовался не метод рехеширования, а метод цепочек (гроздей), который, конечно же, предпочтительнее в системах с динамическим распределением памяти.