Меня интересует сложность времени компилятора. Ясно, что это очень сложный вопрос, так как есть много компиляторов, опций компилятора и переменных, которые необходимо учитывать. В частности, меня интересует LLVM, но меня интересуют любые мысли людей или места, где можно начать исследования. Довольно Google, кажется, мало что дает на свет.
Я предполагаю, что есть некоторые шаги оптимизации, которые являются экспоненциальными, но которые мало влияют на фактическое время. Например, экспоненциальный, основанный на числе, является аргументом функции.
Я бы сказал, что генерация дерева AST будет линейной. Генерация ИК-сигналов потребует перехода по дереву при поиске значений в постоянно растущих таблицах, поэтому или . Генерация кода и связывание будет аналогичным типом операции. Следовательно, мое предположение было бы , если бы мы удалили экспоненты переменных, которые не растут реально.
Я могу быть совершенно не прав, хотя. У кого-нибудь есть мысли по этому поводу?
Ответы:
Вероятно, лучшая книга для ответа на ваш вопрос: 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) d O(n) где - количество трехадресных инструкций.n
Для более сложной оптимизации вы можете захотеть сделать более сложный анализ. В этот момент вы начинаете сталкиваться с компромиссами. Вы хотите, чтобы ваши алгоритмы анализа занимали намного меньше, чемO(n2) время в размере потокового графа всей программы, но это означает, что вам нужно обходиться без информации (и программ, улучшающих преобразования), которые могут оказаться дорогостоящими, чтобы доказать. Классическим примером этого является анализ псевдонимов, где для некоторой пары записей памяти вы хотели бы доказать, что эти две записи никогда не могут предназначаться для одной и той же области памяти. (Возможно, вы захотите выполнить анализ псевдонимов, чтобы увидеть, можете ли вы переместить одну инструкцию над другой.) Но для получения точной информации о псевдонимах вам может потребоваться проанализировать каждый возможный путь управления через программу, который экспоненциально по числу ветвей. в программе (и, следовательно, экспоненциально в количестве узлов в графе потока управления.)
Далее вы попадаете в регистр распределения. Распределение регистров можно сформулировать как проблему окраски графа, а раскраска графа минимальным количеством цветов известна как NP-Hard. Поэтому большинство компиляторов используют какую-то жадную эвристику в сочетании с разливом регистров с целью максимально возможного сокращения количества разливов регистров в разумные сроки.
Наконец-то вы попали в генерацию кода. Генерация кода обычно выполняется максимальным базовым блоком в то время, когда базовый блок представляет собой набор линейно связанных узлов графа потока управления с одним входом и одним выходом. Это можно переформулировать как проблему, покрывающую график, где график, который вы пытаетесь охватить, представляет собой график зависимости набора 3-адресных инструкций в базовом блоке, а вы пытаетесь покрыть набором графиков, которые представляют доступную машину инструкции. Эта проблема экспоненциально связана с размером самого большого базового блока (который, в принципе, может быть в том же порядке, что и размер всей программы), поэтому, как правило, это снова делается с помощью эвристики, когда используется только небольшое подмножество возможных покрытий. рассмотрены.
источник
На самом деле, некоторые языки (например, C ++, Lisp и D) во время компиляции полны по Тьюрингу, поэтому компиляция их вообще неразрешима. Для C ++ это происходит из-за рекурсивной реализации шаблона. Для Lisp и D вы можете выполнить практически любой код во время компиляции, так что вы можете бросить компилятор в бесконечный цикл, если хотите.
источник
Исходя из моего реального опыта работы с компилятором C #, я могу сказать, что для некоторых программ размер выходного двоичного файла растет экспоненциально по сравнению с размером входного источника (это фактически требуется спецификацией C # и не может быть уменьшено), поэтому сложность по времени должно быть как минимум экспоненциальным.
Известно, что общая задача разрешения перегрузки в C # является NP-сложной (и фактическая сложность реализации является по меньшей мере экспоненциальной).
Обработка комментариев к документации XML в источниках C # также требует оценки произвольных выражений XPath 1.0 во время компиляции, что также является экспоненциальным, AFAIK.
источник
class X<A,B,C,D,E> { class Y : X<Y,Y,Y,Y,Y> { Y.Y.Y.Y.Y.Y.Y.Y.Y y; } }
Измерьте это с помощью реалистичных основ кода, таких как набор проектов с открытым исходным кодом. Если вы выводите результаты в виде (codeSize, finishTime), то вы можете построить эти графики. Если ваши данные f (x) = y равны O (n), то построение графика g = f (x) / x должно дать вам прямую линию после того, как данные станут большими.
График f (x) / x, f (x) / lg (x), f (x) / (x * lg (x)), f (x) / (x * x) и т. Д. График будет либо погружаться от нуля, увеличивается без ограничений или выравнивается. Эта идея пригодится для таких ситуаций, как измерение времени вставки, начиная с пустой базы данных (т. Е. Для поиска «утечки производительности» в течение длительного периода времени).
источник