Вероятно, лучшая книга для ответа на ваш вопрос: Cooper and Torczon, «Разработка компилятора», 2003. Если у вас есть доступ к университетской библиотеке, вы можете взять копию.
В производственном компиляторе, таком как llvm или gcc, разработчики прилагают все усилия, чтобы все алгоритмы оставались ниже где - размер ввода. Для некоторых этапов анализа «оптимизации» это означает, что вам нужно использовать эвристику, а не создавать действительно оптимальный код.O(n2)n
Лексер является конечным автоматом, поэтому по размеру ввода (в символах) и создает поток токенов, который передается парсеру.O(n)O(n)
Для многих компиляторов для многих языков синтаксическим анализатором является LALR (1), и поэтому он обрабатывает поток токенов за время в количестве входных токенов. Во время синтаксического анализа вы обычно должны отслеживать таблицу символов, но для многих языков это может быть обработано с помощью стека хеш-таблиц («словарей»). Каждый доступ к словарю - , но вам иногда приходится обходить стек, чтобы найти символ. Глубина стека составляет где - глубина вложения областей. (Так что в языках, подобных C, сколько слоев фигурных скобок у вас внутри.)O(n)O(1)O(s)s
Затем дерево разбора обычно «сплющивается» в граф потока управления. Узлы графа потока управления могут быть трехадресными инструкциями (аналогично языку ассемблера RISC), и размер графа потока управления обычно будет линейным по размеру дерева разбора.
Затем обычно применяется ряд шагов устранения избыточности (устранение общего подвыражения, движение инвариантного кода цикла, постоянное распространение и т. Д.). (Это часто называют «оптимизацией», хотя редко бывает что-либо оптимальное в результате, реальная цель состоит в том, чтобы максимально улучшить код в рамках временных и пространственных ограничений, которые мы наложили на компилятор.) Каждый шаг устранения избыточности будет как правило, требуются доказательства некоторых фактов о графе потока управления. Эти доказательства обычно делаются с использованием анализа потока данных . Большинство анализов потока данных спроектированы так, что они будут сходиться за проходов по потоковому графику, где (грубо говоря) глубина вложения цикла, а проход по потоковому графику занимает времяO(d)dO(n)где - количество трехадресных инструкций.n
Для более сложной оптимизации вы можете захотеть сделать более сложный анализ. В этот момент вы начинаете сталкиваться с компромиссами. Вы хотите, чтобы ваши алгоритмы анализа занимали намного меньше, чемO(n2)время в размере потокового графа всей программы, но это означает, что вам нужно обходиться без информации (и программ, улучшающих преобразования), которые могут оказаться дорогостоящими, чтобы доказать. Классическим примером этого является анализ псевдонимов, где для некоторой пары записей памяти вы хотели бы доказать, что эти две записи никогда не могут предназначаться для одной и той же области памяти. (Возможно, вы захотите выполнить анализ псевдонимов, чтобы увидеть, можете ли вы переместить одну инструкцию над другой.) Но для получения точной информации о псевдонимах вам может потребоваться проанализировать каждый возможный путь управления через программу, который экспоненциально по числу ветвей. в программе (и, следовательно, экспоненциально в количестве узлов в графе потока управления.)
Далее вы попадаете в регистр распределения. Распределение регистров можно сформулировать как проблему окраски графа, а раскраска графа минимальным количеством цветов известна как NP-Hard. Поэтому большинство компиляторов используют какую-то жадную эвристику в сочетании с разливом регистров с целью максимально возможного сокращения количества разливов регистров в разумные сроки.
Наконец-то вы попали в генерацию кода. Генерация кода обычно выполняется максимальным базовым блоком в то время, когда базовый блок представляет собой набор линейно связанных узлов графа потока управления с одним входом и одним выходом. Это можно переформулировать как проблему, покрывающую график, где график, который вы пытаетесь охватить, представляет собой график зависимости набора 3-адресных инструкций в базовом блоке, а вы пытаетесь покрыть набором графиков, которые представляют доступную машину инструкции. Эта проблема экспоненциально связана с размером самого большого базового блока (который, в принципе, может быть в том же порядке, что и размер всей программы), поэтому, как правило, это снова делается с помощью эвристики, когда используется только небольшое подмножество возможных покрытий. рассмотрены.