Межпроцедурный анализ
В этой статье обсуждается межпроцедурный анализ, ключевая концепция разработки компилятора. Основная идея заключается в том, что компиляторы должны учитывать ограничения различных языков и их среды выполнения при создании кода. Эти ограничения можно устранить, выполнив межпроцедурный анализ (IPA), который анализирует поток управления в программе с использованием методов статического анализа. В этой статье мы обсудим, что такое IPA и как его использовать, а также его важность для создания хороших компиляторов.
Графики вызовов
Графы вызовов используются для представления структуры программы. Их можно использовать в качестве инструмента во время анализа, а также для других целей, таких как:
- Поиск и удаление мертвого кода (мертвые ветки).
- Поиск и удаление повторяющегося кода (дублирующиеся условия, которые всегда оцениваются как истинные).
- Поиск и удаление кода, который не используется (код, который никогда не выполняется).
- Поиск и удаление лишнего кода (ненужных переменных или циклов).
Кроме того, блок-схемы — хороший способ документирования структуры вашей программы. Это считается хорошей практикой, когда вы работаете над большими проектами с несколькими членами команды.
Контекстная чувствительность:
Контекстная чувствительность — это способность компилятора находить все возможные пути кода в программе. Это важная особенность современных компиляторов, которая помогает им создавать более эффективные программы за счет сокращения количества решений о ветвлении, принимаемых компилятором, и, таким образом, экономии времени и памяти.
Контекстно-зависимый анализ выполняется путем просмотра значений переменных и параметров в местах их вызова. Если вы измените одно значение во время компиляции, это может повлиять и на другие части вашей программы (например, если вы измените значение чего-то глобального). В этом случае нам необходимо принять во внимание эти изменения, когда мы выполняем наш межпроцедурный анализ позже во время компиляции или даже во время выполнения позже во время выполнения, потому что они потенциально могут сломать некоторые части нашей программы, которые может вызвать проблемы позже при попытке снова выполнить эти конкретные функции/скрипты после того, как они уже были выполнены один раз заранее с другим файлом(ами) настройки скрипта установки.
Строки вызова:
Строки вызова используются для представления последовательности вызовов функций в программе. В общем, строки вызовов можно рассматривать как линеаризацию графа вызовов: они показывают, сколько раз вызывается каждая функция и какие другие функции она вызывает.
Графы вызовов могут быть представлены в виде деревьев, где каждый узел представляет инструкцию (или блок), а каждое ребро представляет переход указателя между инструкциями. Корневой узел этого дерева находится в памяти по адресу 0x00; любые другие узлы после него будут иметь свои собственные адреса, указывающие на этот корневой адрес (который мы назовем «0x00»). Это позволяет нам увидеть, как далеко мы спускаемся в память для каждого уровня, следуя этим путям в обратном направлении через нашу древовидную структуру, пока мы снова не вернемся в 0x00 — этот процесс происходит рекурсивно до тех пор, пока не останется места для новых узлов!
Первое, что мы сделаем, это напишем функцию, которая может принимать граф вызовов и возвращать его строковое представление. Цель этой функции — дать нам возможность отслеживать, сколько раз каждая функция вызывалась, сколько раз возвращалась и какие другие функции вызывались ими (через их аргументы).
Контекстно-зависимый анализ на основе клонирования:
Контекстно-зависимый анализ на основе клонирования — это метод межпроцедурного анализа, который использует граф клонирования для представления объектного кода.
Контекстно-зависимый анализ на основе клонирования можно использовать для сравнения двух программ, скажем, A и B, в разные моменты времени. Например, предположим, что вы хотите определить, есть ли различия между A и B после внесения некоторых изменений. Вы можете выполнить эту задачу, сначала независимо сравнив их исходный код (или промежуточную форму), а затем объединив результаты вместе в абстрактное синтаксическое дерево (AST). Однако для этого потребуется два отдельных прохода по каждой программе, поскольку компиляция происходит на обоих этапах компиляции: один проход во время предварительной обработки, когда исходные файлы анализируются только один раз, а другой проход происходит во время постобработки, когда они снова анализируются после выполнения оптимизации. их путем оптимизации проходов, таких как развертывание цикла и т. д.).
Контекстно-зависимый анализ на основе итогов:
Анализ программы основан на подведении итогов ее выполнения. Это древовидная структура данных, что означает, что ее можно представить разными способами. Анализ использует это представление, чтобы определить, какие действия необходимо выполнить в каждый момент времени.
Реализация компилятора состоит из двух частей: абстрактной машины и работающего поверх нее транслятора в байт-код (или, может быть, другой реализации). Абстрактная машина компилирует программы на промежуточный язык, преобразовывая их в исходный код, который использует только языковые конструкции высокого уровня (такие как ifs), затем переводит эти промежуточные представления в низкоуровневые, прежде чем окончательно выполнить их на оборудовании с соответствующим набором инструкций.
Этап перевода принимает две формы: однопроходный подход, при котором вся информация, необходимая для перевода, доступна заранее; или несколько проходов через входные файлы, когда только некоторые части переводятся сразу, а другие остаются нетронутыми до более поздних этапов, когда для правильной интерпретации может потребоваться дополнительный контекст.
Межпроцедурный анализ в дизайне компилятора:
Межпроцедурный анализ — это процесс анализа программы, переведенной с одного языка на другой. Это делается путем просмотра графов вызовов программы, которые представляют собой графы, показывающие, как различные функции вызываются по отношению друг к другу. Затем компилятор может использовать эту информацию, чтобы определить, насколько хорошо он будет выполнять программу на любой заданной архитектуре машины или операционной системе.
Чтобы межпроцедурный анализ работал должным образом, в вашем исходном коде должна быть какая-то контекстная чувствительность: вы хотите, чтобы модули (функции) и структуры данных вашей программы были не только в их исходной форме, но и со всей необходимой информацией о том, как они были вызваны во время компиляции, чтобы вы могли рассмотреть все возможные сценарии, прежде чем решить, имеет ли что-то смысл или нет при последующем запуске на другой комбинации архитектуры машины/ОС без существенного изменения ее поведения.
So let’s take an example here: Imagine we have written a function which takes two arguments – x and y – but does nothing
else than returning those values back over again; however instead we decide now that instead we’d like our function call itself repeatedly until reaching zero return value; thus resulting into calling this same code multiple times per iteration instead just once per invocation! Would this work?
The answer is: No, it wouldn’t work. The reason why this is so is because when we call a function we expect it to return something back; and if instead what happens is that the called code just keeps on calling itself over and over again without ever stopping then things will eventually spiral out of control until eventually the whole program crashes with an access violation error.