Оптимизирующий компилятор - Википедия - Optimizing compiler

В вычисление, оптимизирующий компилятор это компилятор который пытается минимизировать или максимизировать некоторые атрибуты исполняемый файл компьютерная программа. Общие требования сводятся к минимуму программа время исполнения, объем памяти занимаемая площадь, размер хранилища и мощность потребление (последние три популярны портативные компьютеры ).

Оптимизация компилятора обычно осуществляется с помощью последовательности оптимизация преобразований, алгоритмы, которые принимают программу и преобразуют ее для создания семантически эквивалентной выходной программы, которая использует меньше ресурсов и / или выполняется быстрее. Было показано, что некоторые проблемы оптимизации кода НП-полный, или даже неразрешимый. На практике такие факторы, как программист Готовность ждать, пока компилятор выполнит свою задачу, накладывает верхние пределы на оптимизацию, которую может обеспечить компилятор. Оптимизация вообще очень ЦПУ - и процесс с интенсивным использованием памяти. В прошлом ограничения компьютерной памяти также были основным фактором, ограничивающим возможности выполнения оптимизаций.

Из-за этих факторов оптимизация редко дает «оптимальный» результат в каком-либо смысле, и на самом деле «оптимизация» в некоторых случаях может снизить производительность. Скорее, они представляют собой эвристические методы для улучшения использования ресурсов в типичных программах.[1]

Виды оптимизации

Методы, используемые при оптимизации, можно разделить на различные объемы который может повлиять на все, от одного оператора до всей программы. Вообще говоря, методы с локальным охватом легче реализовать, чем глобальные, но дают меньший выигрыш. Некоторые примеры областей применения включают:

Оптимизация глазка
Обычно они выполняются в конце процесса компиляции после Машинный код был создан. Эта форма оптимизации исследует несколько смежных инструкций (например, «просмотр кода через глазок»), чтобы увидеть, можно ли их заменить одной инструкцией или более короткой последовательностью инструкций.[2] Например, умножение значения на 2 может быть более эффективно выполнено левый значение или добавлением значения к самому себе (этот пример также является экземпляром снижение силы ).
Локальные оптимизации
Они рассматривают только информацию, локальную для базовый блок.[3] Поскольку в базовых блоках нет потока управления, эти оптимизации требуют очень небольшого анализа, что позволяет сэкономить время и уменьшить требования к хранилищу, но это также означает, что информация не сохраняется при переходах.
Глобальные оптимизации
Они также называются «внутрипроцедурными методами» и действуют на целые функции.[3] Это дает им больше информации для работы, но часто требует дорогостоящих вычислений. При вызове функций или обращении к глобальным переменным приходится делать предположения наихудшего случая, поскольку информации о них мало.
Оптимизация цикла
Они действуют на операторы, составляющие цикл, такие как за петля, например движение кода с инвариантным циклом. Оптимизация циклов может иметь значительное влияние, потому что многие программы проводят большую часть своего времени внутри циклов.[4]
Предварительная оптимизация магазина
Это позволяет операциям магазина происходить раньше, чем это было бы разрешено в противном случае в контексте потоки и замки. Процессу необходимо заранее знать, какое значение будет сохранено назначением, которому он должен был следовать. Цель этого ослабления - позволить оптимизации компилятора выполнять определенные виды перестройки кода, которые сохраняют семантику правильно синхронизированных программ.[5]
Межпроцедурные, программные или оптимизация времени компоновки
Они анализируют весь исходный код программы. Большее количество извлекаемой информации означает, что оптимизации могут быть более эффективными по сравнению с тем, когда они имеют доступ только к локальной информации, то есть в рамках одной функции. Такая оптимизация также позволяет применять новые методы. Например, функция встраивание, где вызов функции заменяется копией тела функции.
Оптимизация машинного кода и оптимизатор объектного кода
Они анализируют образ исполняемой задачи программы после того, как весь исполняемый машинный код был связаны. Некоторые из методов, которые могут применяться в более ограниченном объеме, такие как сжатие макросов, которое экономит место за счет сворачивания общих последовательностей инструкций, более эффективны, когда весь образ исполняемой задачи доступен для анализа.[6]

В дополнение к оптимизациям с ограниченным объемом существует еще две общие категории оптимизации:

Язык программирования –Независимый или зависящий от языка
Большинство языков высокого уровня имеют общие программные конструкции и абстракции: решение (если, переключатель, регистр), цикл (для, пока, повторять .. до, делать .. пока) и инкапсуляция (структуры, объекты). Таким образом, аналогичные методы оптимизации могут использоваться для разных языков. Однако некоторые языковые функции затрудняют некоторые виды оптимизации. Например, наличие указателей в C и C ++ затрудняет оптимизацию доступа к массиву (см. анализ псевдонимов ). Однако такие языки, как PL / 1 (который также поддерживает указатели), тем не менее, есть доступные сложные оптимизирующие компиляторы для достижения лучшей производительности различными другими способами. И наоборот, некоторые языковые функции упрощают определенную оптимизацию. Например, на некоторых языках функциям не разрешается иметь побочные эффекты. Следовательно, если программа выполняет несколько вызовов одной и той же функции с одинаковыми аргументами, компилятор может сразу сделать вывод, что результат функции необходимо вычислить только один раз. В языках, где функциям разрешено иметь побочные эффекты, возможна другая стратегия. Оптимизатор может определить, какая функция не имеет побочных эффектов, и ограничить такую ​​оптимизацию функциями без побочных эффектов. Эта оптимизация возможна только тогда, когда оптимизатор имеет доступ к вызываемой функции.
Независимость от машины против зависимости от машины
Многие оптимизации, которые работают с абстрактными концепциями программирования (циклы, объекты, структуры), не зависят от машины, на которую нацелен компилятор, но многие из наиболее эффективных оптимизаций - это те, которые лучше всего используют специальные возможности целевой платформы. Примерами являются инструкции, которые делают несколько вещей одновременно, например, регистр уменьшения и переход, если не равен нулю.

Ниже приведен пример оптимизации, зависящей от локальной машины. Чтобы установить регистр до 0 очевидным способом является использование константы «0» в инструкции, которая устанавливает значение регистра в константу. Менее очевидный способ - XOR зарегистрироваться сам с собой. Компилятор должен знать, какой вариант инструкции использовать. На многих RISC машины, обе инструкции будут одинаково подходящими, поскольку они будут иметь одинаковую длину и занимать одинаковое время. На многих других микропроцессоры такой как Intel x86 семейства, оказывается, что вариант XOR короче и, вероятно, быстрее, так как не будет необходимости ни декодировать непосредственный операнд, ни использовать внутренний «регистр непосредственного операнда». Потенциальная проблема заключается в том, что XOR может ввести зависимость данных от предыдущего значения регистра, что приведет к трубопровод ларек. Однако процессоры часто используют XOR регистра с самим собой как особый случай, который не вызывает остановок.

Факторы, влияющие на оптимизацию

Сама машина
Многие варианты оптимизации могут и должны быть выполнены в зависимости от характеристик целевой машины. Иногда можно параметризовать некоторые из этих машинно-зависимых факторов, так что один фрагмент кода компилятора можно использовать для оптимизации различных машин, просто изменяя параметры машинного описания. GCC компилятор, который иллюстрирует этот подход.
Архитектура целевого процессора
Количество ЦПУ регистры: В определенной степени, чем больше регистров, тем проще оптимизировать производительность. Локальные переменные могут быть размещены в регистрах, а не в куча. Временные / промежуточные результаты можно оставить в регистрах без записи и чтения из памяти.
  • RISC против CISC: Наборы инструкций CISC часто имеют переменную длину инструкций, часто имеют большее количество возможных инструкций, которые можно использовать, и каждая инструкция может занять разное количество времени. Наборы инструкций RISC пытаются ограничить вариативность в каждом из них: наборы инструкций обычно имеют постоянную длину, за некоторыми исключениями, обычно меньше комбинаций регистров и операций с памятью, а также скорость выдачи инструкций (количество инструкций, выполненных за период времени, обычно является целым числом, кратным тактовому циклу) обычно является постоянным в случаях, когда задержка памяти не является фактором. Может быть несколько способов выполнения определенной задачи, при этом CISC обычно предлагает больше альтернатив, чем RISC. Компиляторы должны знать относительную стоимость различных инструкций и выбирать наилучшую последовательность инструкций (см. выбор инструкции ).
  • Трубопроводы: Конвейер - это, по сути, процессор, разбитый на сборочная линия. Он позволяет использовать части ЦП для разных инструкций, разбивая выполнение инструкций на различные этапы: декодирование инструкций, декодирование адреса, выборка из памяти, выборка из регистров, вычисление, хранение регистров и т. Д. Одна инструкция может находиться на этапе хранения регистров. , в то время как другой может находиться на этапе выборки регистра. Конфликты конвейера возникают, когда инструкция на одном этапе конвейера зависит от результата другой инструкции, предшествующей ей в конвейере, но еще не завершенной. Конфликты трубопроводов могут привести к стойла трубопроводов: где ЦП тратит циклы в ожидании разрешения конфликта.
Составители могут графикили измените порядок инструкций, чтобы сбои конвейера происходили реже.
  • Количество функциональных блоков: Некоторые процессоры имеют несколько ALU и FPUs. Это позволяет им выполнять несколько инструкций одновременно. Могут быть ограничения на то, какие инструкции могут сочетаться с какими другими инструкциями («спаривание» - это одновременное выполнение двух или более инструкций) и какой функциональный блок может выполнять какую инструкцию. У них также есть проблемы, похожие на конфликты трубопроводов.
Здесь снова инструкции должны быть запланированы таким образом, чтобы различные функциональные блоки были полностью снабжены инструкциями для выполнения.
Архитектура машины
  • Кеш размер (256 кБ – 12 МБ) и тип (прямое сопоставление, 2- / 4- / 8- / 16-сторонний ассоциативный, полностью ассоциативный): такие методы, как встроенное расширение и разворачивание петли может увеличить размер сгенерированного кода и уменьшить локальность кода. Программа может резко замедлиться, если часто используемый фрагмент кода (например, внутренние циклы в различных алгоритмах) внезапно не помещается в кеш. Кроме того, кеши, которые не являются полностью ассоциативными, имеют более высокие шансы на коллизии кеша даже в незаполненном кеше.
  • Скорость передачи кэша / памяти: они дают компилятору указание на штраф за промахи кеша. Это используется в основном в специализированных приложениях.
Предполагаемое использование сгенерированного кода
Отладка
При написании приложения программист часто перекомпилирует и тестирует, поэтому компиляция должна быть быстрой. Это одна из причин, по которой большинство оптимизаций намеренно избегают на этапе тестирования / отладки. Также программный код обычно «пошаговый» (см. Программная анимация ) используя символический отладчик, а оптимизация преобразований, особенно тех, которые переупорядочивают код, может затруднить сопоставление выходного кода с номерами строк в исходном исходном коде. Это может сбить с толку как инструменты отладки, так и программистов, использующих их.
Универсальное использование
Очень часто ожидается, что предварительно упакованное программное обеспечение будет выполняться на различных машинах и процессорах, которые могут совместно использовать один и тот же набор инструкций, но иметь разные характеристики синхронизации, кеша или памяти. В результате код может не быть настроен на какой-либо конкретный ЦП или может быть настроен так, чтобы он лучше всего работал на самом популярном ЦП, но при этом достаточно хорошо работал на других ЦП.
Специальное использование
Если программное обеспечение скомпилировано для использования на одной или нескольких очень похожих машинах с известными характеристиками, то компилятор может сильно настроить сгенерированный код для этих конкретных машин, при условии, что такие опции доступны. Важные особые случаи включают код, разработанный для параллельно и векторные процессоры, для которых специальные распараллеливание компиляторов работают.
Встроенные системы
Это частый случай специального использования. Встроенное программное обеспечение может быть точно настроено на точный размер процессора и памяти. Кроме того, стоимость или надежность системы могут быть важнее скорости кода. Например, компиляторы для встроенного программного обеспечения обычно предлагают варианты, которые уменьшают размер кода за счет скорости, потому что память - это основная стоимость встроенного компьютера. Время выполнения кода может быть предсказуемым, а не максимально быстрым, поэтому кеширование кода может быть отключено вместе с оптимизацией компилятора, которая требует этого.

Общие темы

В значительной степени методы оптимизации компилятора имеют следующие темы, которые иногда конфликтуют.

Оптимизировать общий случай
Общий случай может иметь уникальные свойства, позволяющие быстрый путь за счет медленный путь. Если использовать быстрый путь чаще всего, в результате улучшается общая производительность.
Избегайте избыточности
Повторно используйте результаты, которые уже вычислены, и сохраните их для дальнейшего использования вместо их повторного вычисления.
Меньше кода
Удалите ненужные вычисления и промежуточные значения. Меньшая нагрузка на ЦП, кэш и память обычно приводит к более быстрому выполнению. В качестве альтернативы в встроенные системы, меньше кода снижает стоимость продукта.
Меньше прыжков за счет использования код прямой линии, также называемый безотраслевой код
Менее сложный код. Прыжки (условные или безусловные ветви ) мешают предварительной выборке инструкций, замедляя код. Использование встраивания или развертывания цикла может уменьшить ветвление за счет увеличения двоичный файл размер на длину повторяющегося кода. Это имеет тенденцию объединять несколько базовые блоки в один.
Местонахождение
Код и данные, к которым осуществляется близкий доступ во времени, должны размещаться в памяти близко друг к другу, чтобы увеличить пространственное местонахождение ссылки.
Используйте иерархию памяти
Доступ к памяти становится все дороже для каждого уровня иерархия памяти, поэтому сначала поместите наиболее часто используемые элементы в регистры, затем в кеши, а затем в основную память, прежде чем они будут записаны на диск.
Распараллелить
Измените порядок операций, чтобы позволить нескольким вычислениям происходить параллельно на уровне инструкций, памяти или потока.
Более точная информация лучше
Чем точнее информация компилятора, тем лучше он может использовать любой или все эти методы оптимизации.
Показатели времени выполнения могут помочь
Информация, собранная во время пробного запуска, может быть использована в профильная оптимизация. Информация, собираемая во время выполнения, в идеале с минимальным накладные расходы, может использоваться JIT компилятор для динамического улучшения оптимизации.
Снижение силы
Замените сложные или трудные или дорогие операции более простыми. Например, замена деления на константу умножением на обратную величину или использование индукционный анализ переменных заменить умножение на индекс цикла сложением.

Конкретные техники

Оптимизация цикла

Некоторые методы оптимизации, в первую очередь предназначенные для работы с циклами, включают:

Индукционный анализ переменных
Грубо говоря, если переменная в цикле является простой линейной функцией индексной переменной, например j: = 4 * я + 1, он может обновляться соответствующим образом каждый раз при изменении переменной цикла. Это снижение силы, а также может позволить определениям индексных переменных стать мертвый код.[7] Эта информация также полезна для исключение проверки границ и анализ зависимости, среди прочего.
Деление петли или петлевое распределение
Деление цикла пытается разбить цикл на несколько циклов в одном и том же диапазоне индексов, но каждый из них занимает только часть тела цикла. Это может улучшить местонахождение ссылки, как данные, к которым осуществляется доступ в цикле, так и код в теле цикла.
Петля слияния или объединение петель, или набивка петли, или заклинивание петли
Другой метод, который пытается уменьшить накладные расходы цикла. Когда два смежных цикла будут повторяться одинаковое количество раз, независимо от того, известно ли это число во время компиляции, их тела могут быть объединены, если они не ссылаются на данные друг друга.
Инверсия петли
Этот метод меняет стандарт пока петля в делать пока (также известен как повторять / пока), завернутый в если условный, уменьшающий количество переходов на два, для случаев, когда цикл выполняется. Это дублирует проверку условия (увеличивает размер кода), но более эффективно, поскольку переходы обычно вызывают ошибку. стойло трубопровода. Кроме того, если начальное условие известно во время компиляции и известно как побочный эффект -бесплатно если охранник можно пропустить.
Петлевой обмен
Эти оптимизации меняют внутренние циклы на внешние. Когда переменные цикла индексируются в массиве, такое преобразование может улучшить локальность ссылки в зависимости от макета массива.
Циклически инвариантное движение кода
Если количество вычисляется внутри цикла во время каждой итерации, и его значение одинаково для каждой итерации, это может значительно повысить эффективность, подняв его за пределы цикла и вычислив его значение только один раз перед началом цикла.[4] Это особенно важно для выражений вычисления адресов, генерируемых циклами по массивам. Для правильной реализации эту технику необходимо использовать с инверсия петли, потому что не весь код можно безопасно поднимать за пределы цикла.
Оптимизация гнезда петель
Некоторые распространенные алгоритмы, такие как умножение матриц, имеют очень плохое поведение кеша и чрезмерное обращение к памяти. Оптимизация вложенности циклов увеличивает количество попаданий в кэш за счет выполнения операции над небольшими блоками и использования обмена циклами.
Разворот петли
Реверсирование цикла меняет порядок, в котором значения присваиваются индексной переменной. Это тонкая оптимизация, которая может помочь устранить зависимости и, таким образом, включить другие оптимизации. Кроме того, на некоторых архитектурах реверсирование цикла способствует уменьшению кода, так как, когда индекс цикла уменьшается, условие, которое должно быть выполнено для того, чтобы запущенная программа вышла из цикла, - это сравнение с нулем. Часто это специальная инструкция без параметров, в отличие от сравнения с числом, для которого требуется число для сравнения. Таким образом, количество байтов, необходимых для хранения параметра, сохраняется за счет обращения цикла. Кроме того, если число для сравнения превышает размер слова платформы, в стандартном порядке цикла потребуется выполнить несколько инструкций, чтобы оценить сравнение, что не относится к реверсированию цикла.
Развертывание петли
При развертывании тело цикла дублируется несколько раз, чтобы уменьшить количество проверок условия цикла и количество переходов, которые ухудшают производительность из-за ухудшения конвейера команд. Оптимизация "меньше скачков". Полное развертывание цикла устраняет все накладные расходы, но требует, чтобы количество итераций было известно во время компиляции.
Расщепление петли
Разделение цикла пытается упростить цикл или устранить зависимости, разбивая его на несколько циклов, которые имеют одинаковые тела, но повторяются по разным смежным частям диапазона индекса. Полезный частный случай: пилинг петли, который может упростить цикл с проблемной первой итерацией, выполняя эту итерацию отдельно перед входом в цикл.
Отключение петли
Отключение перемещает условное выражение изнутри цикла за пределы цикла путем дублирования тела цикла внутри каждого из предложений if и else условного выражения.
Конвейерная обработка программного обеспечения
Цикл реструктурирован таким образом, что работа, выполняемая в итерации, разделяется на несколько частей и выполняется в течение нескольких итераций. В замкнутом цикле этот метод скрывает задержку между загрузкой и использованием значений.
Автоматическое распараллеливание
Цикл преобразуется в многопоточный или векторизованный (или даже в оба) код для одновременного использования нескольких процессоров в многопроцессорной машине с общей памятью (SMP), включая многоядерные машины.

Оптимизация потока данных

Поток данных оптимизации, основанные на анализ потока данных, в первую очередь зависят от того, как определенные свойства данных распространяются контрольными ребрами в график потока управления. Некоторые из них включают:

Исключение общего подвыражения
В выражении (а + б) - (а + б) / 4, "общее подвыражение" относится к дублированному (а + б). Компиляторы, реализующие эту технику, понимают, что (а + б) не изменится, поэтому рассчитайте его значение только один раз.[8]
Постоянное сворачивание и размножение[9]
заменяя выражения, состоящие из констант (например, 3 + 5) с их окончательным значением (8) во время компиляции, а не во время выполнения. Используется на большинстве современных языков.
Распознавание и устранение индукционных переменных
см. обсуждение выше о индукционный анализ переменных.
Классификация псевдонимов и анализ указателей
в присутствии указатели, вообще сложно провести какую-либо оптимизацию, поскольку потенциально любая переменная может быть изменена при назначении области памяти. Указав, какие указатели могут быть псевдонимами каких переменных, несвязанные указатели можно игнорировать.
Мертвый магазин устранение
удаление присваиваний переменным, которые впоследствии не читаются, либо из-за окончания срока жизни переменной, либо из-за последующего присваивания, которое перезапишет первое значение.

Оптимизация на основе SSA

Эти оптимизации предназначены для выполнения после преобразования программы в специальную форму, называемую Статическое одиночное присвоение, в котором каждая переменная присваивается только в одном месте. Хотя некоторые из них работают без SSA, они наиболее эффективны с SSA. Многие оптимизации, перечисленные в других разделах, также не требуют особых изменений, таких как распределение регистров.

Нумерация глобальных значений
GVN устраняет избыточность, создавая график значений программы, а затем определить, какие значения вычисляются с помощью эквивалентных выражений. GVN может определить некоторую избыточность, которая исключение общего подвыражения не может, и наоборот.
Редкое распространение условных констант
Сочетает постоянное распространение, постоянное сворачивание, и устранение мертвого кода, и улучшает то, что возможно, если запускать их отдельно.[10][11] Эта оптимизация символически выполняет программу, одновременно распространяя постоянные значения и удаляя части график потока управления что это делает недостижимым.

Оптимизация генератора кода

Размещение регистров
Наиболее часто используемые переменные следует хранить в регистрах процессора для максимально быстрого доступа. Чтобы найти, какие переменные поместить в регистры, создается граф интерференции. Каждая переменная является вершиной, и когда две переменные используются одновременно (имеют пересекающийся диапазон значений), между ними есть ребро. Этот график раскрашен, например, Алгоритм Чайтина используя то же количество цветов, что и регистры. Если раскрашивание не удается, одна переменная «переливается» в память и раскрашивание повторяется.
Выбор инструкции
Большинство архитектур, особенно CISC архитектур и тех, у кого много режимы адресации, предлагают несколько различных способов выполнения конкретной операции с использованием совершенно разных последовательностей инструкций. Задача селектора инструкций состоит в том, чтобы в целом хорошо выполнять работу по выбору инструкций для реализации каких операторов на низком уровне. промежуточное представление с. Например, на многих процессорах в 68000 семья а в архитектуре x86 сложные режимы адресации могут использоваться в таких операторах, как «lea 25 (a1, d5 * 4), a0», что позволяет одной инструкции выполнять значительный объем арифметических операций с меньшим объемом памяти.
Планирование инструкций
Планирование инструкций - важная оптимизация для современных конвейерных процессоров, которая позволяет избежать остановок или пузырей в конвейере за счет кластеризации инструкций без зависимостей вместе, сохраняя при этом исходную семантику.
Рематериализация
Рематериализация пересчитывает значение вместо загрузки из памяти, предотвращая доступ к памяти. Это выполняется в тандеме с распределением регистров, чтобы избежать утечек.
Факторинг кода
Если несколько последовательностей кода идентичны или могут быть параметризованы или переупорядочены для идентичности, их можно заменить вызовами общей подпрограммы. Это часто может совместно использовать код для настройки подпрограммы и иногда хвостовой рекурсии.[12]
Батуты (Батут (компьютерный) )
Много[нужна цитата ] ЦП имеют меньшие инструкции вызова подпрограмм для доступа к малой памяти. Компилятор может сэкономить место, используя эти небольшие вызовы в основной части кода. Инструкции перехода в малой памяти могут обращаться к подпрограммам по любому адресу. Это увеличивает экономию места за счет факторинга кода.[12]
Изменение порядка вычислений
На основе целочисленное линейное программирование, реструктуризация компиляторов улучшает локальность данных и раскрывает больший параллелизм за счет переупорядочения вычислений. Компиляторы, оптимизирующие пространство, могут переупорядочивать код, чтобы удлинить последовательности, которые могут быть включены в подпрограммы.

Оптимизация функционального языка

Хотя многие из них также применимы к нефункциональным языкам, они либо зародились в, либо особенно важны в функциональные языки Такие как Лисп и ML.

Оптимизация хвостового вызова
Вызов функции занимает пространство стека и включает некоторые накладные расходы, связанные с передачей параметров и очисткой кеша инструкций. Рекурсивный хвост алгоритмы могут быть преобразованы в итерация через процесс, называемый устранением хвостовой рекурсии или оптимизацией хвостового вызова.
Вырубка леса (структура данных слияние)
В языках, где обычно применяется последовательность преобразований к списку, вырубка леса пытается удалить создание промежуточных структур данных.
Частичная оценка

Прочие оптимизации

Устранение проверки границ
Многие языки, такие как Ява, обеспечить соблюдение проверка границ всех обращений к массиву. Это тяжелый спектакль горлышко бутылки в некоторых приложениях, например в научном коде. Исключение проверки границ позволяет компилятору безопасно удалять проверку границ во многих ситуациях, когда он может определить, что индекс должен попадать в допустимые границы; например, если это простая переменная цикла.
Оптимизация смещения ответвлений (зависит от станка)
Выберите самое короткое смещение ветви, которое достигает цели
Изменение порядка блоков кода
Переупорядочивание блоков кода изменяет порядок основных блоки в программе, чтобы уменьшить количество условных переходов и улучшить локальность ссылок.
Устранение мертвого кода
Удаляет инструкции, которые не влияют на поведение программы, например, определения, которые не используются, называемые мертвый код. Это уменьшает размер кода и устраняет ненужные вычисления.
Выведение инвариантов (инварианты цикла )
Если выражение выполняется как при соблюдении условия, так и при его невыполнении, оно может быть записано только один раз вне условного оператора. Точно так же, если определенные типы выражений (например, присвоение константы переменной) появляются внутри цикла, их можно переместить из него, потому что их эффект будет одинаковым, независимо от того, выполняются ли они много раз или только один раз. . Также известно как полное устранение избыточности. Более мощная оптимизация частичное устранение избыточности (ПРЕД).
Встроенное расширение или макрос расширение
Когда какой-то код вызывает процедура, можно напрямую вставить тело процедуры в вызывающий код, а не передавать ему управление. Это экономит накладные расходы, связанные с вызовами процедур, а также предоставляет прекрасные возможности для множества различных оптимизаций, зависящих от параметров, но за счет экономии места; тело процедуры дублируется каждый раз, когда процедура вызывается встроенной. Как правило, встраивание полезно в критичном к производительности коде, который выполняет большое количество вызовов небольших процедур. Оптимизация "меньше скачков". В заявления из императивное программирование языки также являются примером такой оптимизации. Хотя операторы могут быть реализованы с помощью вызовы функций они почти всегда реализуются с встраиванием кода.
Прыжковая резьба
В этом проходе объединяются последовательные условные переходы, полностью или частично основанные на одном и том же условии.
Например., если (c) { фу; } если (c) { бар; } к если (c) { фу; бар; },
и если (c) { фу; } если (!c) { бар; } к если (c) { фу; } еще { бар; }.
Макро сжатие
Оптимизация пространства, которая распознает общие последовательности кода, создает подпрограммы («макросы кода»), содержащие общий код, и заменяет вхождения общих кодовых последовательностей вызовами соответствующих подпрограмм.[6] Наиболее эффективно это делается как оптимизация машинного кода, когда присутствует весь код. Этот метод был впервые использован для экономии места в интерпретируемом потоке байтов, используемом в реализации Макро Спитбол на микрокомпьютерах.[13] Проблема определения оптимального набора макросов, который минимизирует пространство, необходимое для данного сегмента кода, известна как НП-полный,[6] но эффективная эвристика дает почти оптимальные результаты.[14]
Уменьшение коллизий кеша
(например, нарушая выравнивание на странице)
Уменьшение высоты штабеля
Измените структуру дерева выражений, чтобы минимизировать ресурсы, необходимые для оценки выражения.
Повторный заказ теста
Если у нас есть два теста, которые являются условием для чего-то, мы можем сначала иметь дело с более простыми тестами (например, сравнивая переменную с чем-то) и только затем со сложными тестами (например, с теми, которые требуют вызова функции). Эта техника дополняет ленивая оценка, но может использоваться только тогда, когда тесты не зависят друг от друга. Короткое замыкание семантика может сделать это трудным.

Межпроцедурные оптимизации

Межпроцедурная оптимизация работает со всей программой, вне зависимости от процедур и границ файлов. Он работает в тесном взаимодействии с внутрипроцедурными аналогами, реализуясь при сотрудничестве локальной и глобальной частей. Типичные межпроцедурные оптимизации: процедура встраивание, устранение межпроцедурного мертвого кода, межпроцедурное распространение констант и переупорядочивание процедуры. Как обычно, компилятор должен выполнить межпроцедурный анализ перед фактической оптимизацией. Межпроцедурный анализ включает анализ псевдонимов, анализ доступа к массиву, а построение график звонков.

Межпроцедурная оптимизация распространена в современных коммерческих компиляторах от SGI, Intel, Microsoft, и Sun Microsystems. Долгое время открытый исходный код GCC подвергся критике[нужна цитата ] из-за отсутствия мощного межпроцедурного анализа и оптимизации, хотя сейчас ситуация улучшается.[нужна цитата ] Другой компилятор с открытым исходным кодом с полной инфраструктурой анализа и оптимизации - это Открыть64.

Из-за дополнительного времени и места, необходимого для межпроцедурного анализа, большинство компиляторов не выполняют его по умолчанию. Пользователи должны явно использовать параметры компилятора, чтобы указать компилятору включить межпроцедурный анализ и другие дорогостоящие оптимизации.

Практические соображения

Компилятор может выполнять широкий спектр оптимизаций, от простых и понятных, требующих небольшого времени компиляции, до сложных и сложных, требующих значительного времени компиляции.[15] Соответственно, компиляторы часто предоставляют параметры своей управляющей команде или процедуре, чтобы позволить пользователю компилятора выбрать, какую оптимизацию запрашивать; например, компилятор IBM FORTRAN H позволял пользователю не указывать оптимизацию, оптимизацию только на уровне регистров или полную оптимизацию.[16] К 2000-м годам это было обычным явлением для компиляторов, таких как Лязг, чтобы иметь ряд параметров команд компилятора, которые могут повлиять на различные варианты оптимизации, начиная с знакомого -O2 выключатель.[17]

Подход к изолированной оптимизации заключается в использовании так называемых постпроходные оптимизаторы (некоторые коммерческие версии относятся к ПО для мэйнфреймов конца 1970-х годов).[18] Эти инструменты берут исполняемый файл оптимизирующим компилятором и оптимизируют его еще больше. Оптимизаторы после прохода обычно работают на язык ассемблера или же Машинный код уровень (в отличие от компиляторов, оптимизирующих промежуточное представление программ). Одним из таких примеров является Портативный компилятор C (pcc) 1980-х годов, у которого был необязательный проход, который выполнял пост-оптимизацию сгенерированного кода сборки.[19]

Еще одно соображение заключается в том, что алгоритмы оптимизации сложны и, особенно при использовании для компиляции больших сложных языков программирования, могут содержать ошибки, которые вносят ошибки в сгенерированный код или вызывают внутренние ошибки во время компиляции. Ошибки компилятора любого рода могут сбивать с толку пользователя, но особенно в этом случае, поскольку может быть неясно, что логика оптимизации неисправна.[20] В случае внутренних ошибок проблема может быть частично решена с помощью "отказоустойчивого" метода программирования, в котором логика оптимизации в компиляторе закодирована так, что сбой фиксируется, выдается предупреждающее сообщение и остальная часть компиляции переходит к успешному завершению.[21]

История

Ранние компиляторы 1960-х годов часто были в первую очередь озабочены простой и правильной компиляцией кода, поэтому время компиляции было главной проблемой. Одним из примечательных ранних оптимизирующих компиляторов был компилятор IBM FORTRAN H конца 1960-х годов.[16] Еще одним из первых и важных оптимизирующих компиляторов, в котором впервые было предложено несколько передовых методов, был компилятор для БЛАЖЕНСТВО (1970), который был описан в Дизайн оптимизирующего компилятора (1975).[22] К концу 1980-х годов оптимизирующие компиляторы оказались достаточно эффективными, поэтому программирование на языке ассемблера пришло в упадок. Это произошло вместе с разработкой чипов RISC и расширенных функций процессора, таких как планирование инструкций и спекулятивное выполнение, которые были разработаны для оптимизации компиляторов, а не для написанного человеком кода сборки.[нужна цитата ]

Список статических анализов кода

Смотрите также

Рекомендации

  1. ^ Ахо, Альфред V .; Сетхи, Рави; Ульман, Джеффри Д. (1986). Компиляторы: принципы, методы и инструменты. Ридинг, Массачусетс: Эддисон-Уэсли. п. 585. ISBN  0-201-10088-6.
  2. ^ Ахо, Сетхи и Ульман, Компиляторы, п. 554.
  3. ^ а б Купер, Кейт Д.; Торчон, Линда (2003) [2002-01-01]. Разработка компилятора. Морган Кауфманн. С. 404, 407. ISBN  978-1-55860-698-2.
  4. ^ а б Ахо, Сетхи и Ульман, Компиляторы, п. 596.
  5. ^ "MSDN - Действия с предыдущим магазином". Microsoft. Получено 2014-03-15.
  6. ^ а б c Клинтон Ф. Госс (август 2013 г.) [Впервые опубликовано в июне 1986 г.]. «Оптимизация машинного кода - улучшение исполняемого объектного кода» (PDF) (Кандидатская диссертация). Технический отчет отдела компьютерных наук № 246. Куранта, Нью-Йоркский университет. arXiv:1308.4815. Bibcode:2013arXiv1308.4815G. Получено 22 августа 2013. Сложить резюме. Цитировать журнал требует | журнал = (помощь)
  7. ^ Ахо, Сетхи и Ульман, КомпиляторыС. 596–598.
  8. ^ Ахо, Сетхи и Ульман, Компиляторы, pp. 592–594.
  9. ^ Стивен Мучник; Muchnick and Associates (15 August 1997). Расширенная реализация проекта компилятора. Морган Кауфманн. стр.329 –. ISBN  978-1-55860-320-2. constant folding.
  10. ^ Wegman, Mark N. and Zadeck, F. Kenneth. "Constant Propagation with Conditional Branches." Транзакции ACM по языкам и системам программирования, 13(2), April 1991, pages 181-210.
  11. ^ Click, Clifford and Cooper, Keith. "Combining Analyses, Combining Optimizations ", Транзакции ACM по языкам и системам программирования, 17(2), March 1995, pages 181-196
  12. ^ а б Cx51 Compiler Manual, version 09.2001, p155, Keil Software Inc.
  13. ^ Robert B. K. Dewar; Мартин Чарльз Голумбик; Клинтон Ф. Госс (август 2013 г.) [Впервые опубликовано в октябре 1979 г.]. МИКРО СПИТБОЛ. Технический отчет отдела компьютерных наук. № 11. Курантский институт математических наук. arXiv:1308.6096. Bibcode:2013arXiv1308.6096D.
  14. ^ Мартин Чарльз Голумбик; Robert B. K. Dewar; Клинтон Ф. Госс (1980). «Макрозамены в МИКРО СПИТБОЛ - комбинаторный анализ». Proc. 11-я Юго-Восточная конференция по комбинаторике, теории графов и вычислениям, Congressus Numerantium, Utilitas Math., Виннипег, Канада. 29: 485–495.
  15. ^ Aho, Sethi, and Ullman, Компиляторы, п. 15.
  16. ^ а б Aho, Sethi, and Ullman, Компиляторы, п. 737.
  17. ^ Guelton, Serge (August 5, 2019). "Customize the compilation process with Clang: Optimization options". Красная шляпа.
  18. ^ Software engineering for the Cobol environment. Portal.acm.org. Проверено 10 августа 2013.
  19. ^ Aho, Sethi, and Ullman, Компиляторы, п. 736.
  20. ^ Sun, Chengnian; и другие. (July 18–20, 2016). "Toward understanding compiler bugs in GCC and LLVM". ISSTA 2016: Proceedings of the 25th International Symposium on Software Testing and Analysis. Issta 2016: 294–305. Дои:10.1145/2931037.2931074. ISBN  9781450343909. S2CID  8339241.
  21. ^ Schilling, Jonathan L. (August 1993). "Fail-safe programming in compiler optimization". Уведомления ACM SIGPLAN. 28 (8): 39–42. Дои:10.1145/163114.163118. S2CID  2224606.
  22. ^ Aho, Sethi, and Ullman, Компиляторы, pp. 740, 779.

внешняя ссылка