Ограничения планирования кода

Опубликовано: 15 Февраля, 2023

Анализ зависимости от данных используется для определения порядка выполнения инструкций. Он используется для определения порядка выполнения инструкций, поскольку указывает, сколько усилий потребуется для выполнения конкретной инструкции в любой момент времени. Зависимая от данных инструкция может быть выполнена только после завершения всех зависимых от нее инструкций.

Настоящая зависимость данных существует, если для выполнения инструкции требуются оба операнда, что означает, что ни один из операндов не может быть пропущен во время ее выполнения, не влияя на поведение программы (т. е. без ошибки). Зависимость от данных существует, когда один операнд не требуется для выполнения инструкции, но другой может изменить свое значение и повлиять на поведение программы (т. е. ошибки). Выходная зависимость существует, когда несколько операторов ассемблера зависят от получения постоянного значения от другого оператора ассемблера; это означает, что если мы изменим это постоянное значение, то наши выходные данные изменятся соответствующим образом, даже если на них не повлияли наши предыдущие изменения значений!

Поиск зависимостей между доступами к памяти:

Анализ зависимости данных массива:

Чтобы найти зависимость данных массива, нам необходимо провести анализ массивов. Мы можем использовать DSA (анализ структуры данных) или MRA (анализ ссылок на память). Общая идея этого метода состоит в том, чтобы найти все различные способы доступа к элементам массива в вашей программе и их зависимости. Самый популярный способ сделать это — использовать графическое представление, называемое «деревом», которое показывает, как каждая переменная влияет на другие переменные.

Анализ псевдонимов указателей:

Псевдоним указателя происходит, когда два указателя указывают на одно и то же место в памяти; они ссылаются на разные объекты, но содержат ссылки, указывающие непосредственно на адреса друг друга. Этот тип проблемы возникает, когда вы создаете несколько объектов с одинаковыми именами или структурами, но с разными структурными макетами.

Например, создаются два списка, один из которых содержит больше элементов, чем другой список (более короткий список был урезан), что приводит к проблемам, когда мы пытаемся получить доступ к этим элементам позже по пути нашего кода!

Проблема в том, что компилятор не знает, к какому списку мы хотим получить доступ; это может быть любой из них. В результате компилятор будет генерировать код, который не является потокобезопасным и небезопасным.

Решение этой проблемы заключается в использовании метода, называемого анализом псевдонимов указателей . Этот метод просматривает ваш код и выясняет, какие указатели указывают на одни и те же ячейки памяти, и следит за тем, чтобы никакие два указателя не указывали на одно и то же место. Он делает это, выполняя простую операцию, называемую « разрешение конфликтов », в которой он находит все возможные конфликты между экземплярами объектов, которые могут быть разделены между потоками.

Компромисс между использованием регистров и параллелизмом:

Компилятор отвечает за поиск компромисса между использованием регистров и параллелизмом. Использование регистров относится к количеству регистров, которые конкретная инструкция использует во время выполнения, а параллелизм относится к тому, сколько инструкций может выполняться параллельно одним процессором (или несколькими процессорами).

Рекомендуемая стратегия для достижения наилучшей производительности часто называется « профилирование на уровне регистров », что означает измерение того, сколько времени ваша программа тратит на использование каждого регистра во время выполнения, а затем модификацию вашего кода, чтобы он работал более эффективно на этих ограниченных ресурсах. Хорошим примером может быть, если у вас есть такая инструкция, как ADD_EQ, которая требует двух операндов: A и B; это означает, что вам нужно, чтобы оба этих регистра были доступны одновременно при вычислении сложения или вычитания, вместо того, чтобы оставлять свободным только один для других целей, таких как сохранение данных в памяти или передача аргументов из обратных вызовов функций вниз через циклы, где это необходимо. Это можно улучшить, поместив два операнда в отдельные регистры, что позволит вам временно использовать один из них при выполнении над ним других инструкций, а затем получить результат вашего вычисления позже. Если процессор имеет только один доступный регистр в любой момент времени, то это известно как « давление регистров » или « конфликт регистров ».

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

Упорядочивание фаз между распределением регистров и планированием кода

Распределение регистров и планирование кода — это две фазы компилятора. Распределение регистров — это первая фаза, которая выделяет регистры для конкретной инструкции, тогда как планирование кода относится ко второй фазе, когда инструкции помещаются на машинный язык. Две фазы независимы друг от друга; однако их можно выполнять в любом порядке или чередовать друг с другом.

Процесс распределения реестра состоит из трех шагов:

  1. Добавить регистры (режим адресации)
  2. Выделить новый регистр (распределитель)
  3. Присвойте значение этим регистрам на основе их использования различными инструкциями в тексте программы (назначение).

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

В отличие от распределения регистров, где для каждой ветви может быть назначено только одно значение, для условно выполняемой команды перехода требуется только два семантически эквивалентных операнда, но все же требуется три такта общего времени обработки из-за дополнительных явных тактов, требуемых при использовании вместе с аппаратными компонентами явного генерирования тактового сигнала. вместо того, чтобы использоваться исключительно в сочетании с предварительно скомпилированными библиотеками, которые сами по себе обычно содержат много ветвей, поскольку большинство программ не имеют значительных частей, состоящих в основном из нескольких больших фрагментов, таких как процедура main(), может потребовать сотни тысяч строк накладных расходов времени выполнения на счетчик цикла итерации выполнение N итераций, прежде чем, наконец, достичь условно выполненного раздела конечной точки ».

Зависимость от управления

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

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

Зависимость от контроля возникает, когда нет необходимости выполнять два независимых вычисления вместе (например, A и B), но оба они могут выполняться одновременно, не влияя на результаты друг друга (например, AB). Этот тип ограничения часто называют «совместным использованием данных», поскольку он позволяет нескольким вычислениям совместно использовать доступ к общим ресурсам, таким как переменные и регистры, не мешая друг другу с помощью неявных механизмов взаимного исключения, таких как барьеры памяти или механизмы синхронизации, такие как критические разделы в потоках; однако все еще может быть некоторая степень взаимных помех из-за того, что один поток может попытаться получить доступ до того, как другой поток закончит использовать эти ресурсы!

Поддержка спекулятивного исполнения:

Спекулятивное выполнение — это когда процессор делает предположения о будущем состоянии своей среды (т. е. инструкции, которые выполняются на других процессорах), а затем выполняет эти инструкции на основе этих предположений. Результат этого процесса обычно более эффективен, чем все предположения, сделанные сразу; однако существуют затраты, связанные с принятием таких решений во время выполнения, а не с ожиданием их выполнения во время выполнения программы (как в обычном коде). Спекулятивные выполнения часто рассматриваются как альтернативный подход, потому что они позволяют программистам принимать решения во время выполнения до того, как они им действительно понадобятся, вместо того, чтобы ждать, пока все уже будет выполнено!

Термин спекулятивное выполнение используется для описания способности процессора делать предположения о будущих инструкциях. Это можно сделать с помощью параллелизма на уровне потоков, который позволяет одновременно выполнять несколько инструкций из разных потоков выполнения.

Базовая модель машины:

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

Код-планирование

Планирование кода — важная часть разработки компилятора. Необходимо знать больше о процессе планирования кода, потому что это может помочь вам убедиться, что ваш компилятор создает оптимальные машины для программ.

В планировании кода есть три шага:

  1. Определите все инструкции, которые необходимо запланировать перед каждым переходом, и их положение относительно друг друга (например, есть две инструкции после инструкции LBR).
  2. Определите, когда эти инструкции должны фактически выполняться, на основе значений счетчика программы и другой информации, такой как таблицы прогнозирования ветвлений, информация об использовании регистров и т. д., что затем позволяет нам принимать решения о том, сколько времени мы хотим, чтобы каждый тип инструкции тратился на выполнение, прежде чем переходить вперед. опять таки.
  3. Сгенерируйте машинный код из нашего алгоритма, чтобы все инструкции выполнялись именно тогда, когда они были предназначены, без каких-либо остановок или коллизий, вызванных главным образом тем, что в нашей архитектуре микропроцессора недостаточно доступных регистров.

Вывод

Как видно из вышеизложенного, компилятор является очень важной частью языка программирования. Это полезно для быстрой и плавной работы программ. Но нужно также знать об ограничениях, налагаемых аппаратным обеспечением на эти машины. Эти ограничения могут не одинаково влиять на все компиляторы; некоторые могут быть более мощными, чем другие, в то время как другие просто не могут с ними справиться. Поэтому, если вы хотите писать программы, совместимые с программами, доступными сегодня или в будущих поколениях, то есть определенные вещи, о которых необходимо позаботиться, прежде чем писать хотя бы одну строку кода.