Опубликован: 19.04.2025 | Доступ: свободный | Студентов: 0 / 0 | Длительность: 01:25:00
Лекция 1:

Постановка задачи

Лекция 1 || Лекция 2 >

Исполнитель, для которого собирается программа обычно называют целевой машиной (англ. target machine). Она состоит в первую очередь из процессора, который постоянно исполняет некоторый машинный код. Код состоит из инструкций, а множество доступных инструкций и их поведение задается с помощью архитектуры набора команд (англ. Instruction Set Architecture, ISA). Машинный код, приготовленный для конкретной ISA должен корректно работать на всех машинах и процессорах, которые поддерживают данную ISA. Конкретные инструкции бывают разнообразные, простые и сложные.

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

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

Компилятор

Обычно, в архитектуре компилятора можно выделить нескольких частей. Всё начинается с синтаксического анализа, где разбирается исходный текст программы, отлавливаются синтаксические ошибки, ошибки типизации и программа преобразовывается в некоторое промежуточное представление (англ. intermediate representation). За это отвечает часть компилятора именуемая frontend.

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

После оптимизаций представление программы передается в back-end, где происходит порождение кода, обычно в три этапа.

  • Выбор инструкций (англ. instruction selection), которые будут исполняться в процессоре.
  • Переупорядочивание инструкций (англ. instruction scheduling), чтобы задействовать все мощности параллелизма конвейера процессора.
  • Распределение регистров (англ. register allocation) для использования встроенных в процессоров регистров и сокращения нагрузки на память.

Выбор инструкций

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

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

  • Поиск шаблонов (англ. pattern matching). Здесь мы ищем все последовательности-кандидаты в порождаемый код. Обычно доступные инструкции пересекаются по демонстрируемому поведению, из-за чего кандидатов может получаться много.
  • Выбор шаблонов (англ. pattern selection) заключается в непосредственном выборе из кандидатов.

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

Сравнение разных методов выбора инструкций

В ISA описаны множества инструкций и они разные по сложности. В самых первых процессорах простыми считались инструкции, работающие с регистрами, а сложными --- работающие с памятью. В литературе упоминаются различные схемы адресации, которые позволяют сокращать размер кода и улучшать производительность. Например, предположим, что нам надо из массива байт загрузить некоторый элемент. По сути у нас есть базовый адрес начала массива и некоторое смещение, и нам нужно сложить эти два адреса и загрузить из памяти по адресу суммы. Для RISC-V мы это должны сделать буквально, а AMD64 имеет специальные инструкции для индексированной адресации. Поддержка такого вида адресации в современных компиляторах давно реализована, поэтому в современной литературе сложными считаются инструкции, у которых много результатов, или те, которые можно использовать только в определенных ситуациях.

Классификация инструкций

Чтобы проще сравнивать различные подходы к выбору инструкций, давайте введем классы инструкций.

С единичным результатом (англ. single-output instructions). Такие инструкции производят только один наблюдаемый результат, который можно прочитать другими инструкциями в ассемблерном коде. Сюда относятся большинство инструкций в современных процессорах, например, сложение и умножения, загрузка из памяти с учетом индекса выше, в том числе сложные инструкции типа cpop из RISCV, которая считает количество единиц в битовом представлении числа.

Обычно, из таких простых инструкций состоят ISA RISC процессоров, например, MIPS или RISC-V с базовым набором инструкций.

С множественными результатами (англ. multi-output instructions) имеют более одного наблюдаемого результата. Классическим примером будут инструкции, которые сразу вычисляют и остаток, и частное, или арифметические инструкции, выставляющие флаги переполнения. Большинство архитектур предоставляют такого рода инструкции, в том числе и AMD64, и RISC-V (например, расширение atomic).

С не пересекающимися результатами (англ. disjoint-output instructions) порождают из набора входных данных набор выходных. От предыдущего вида они отличаются тем, что тут результат не зависит от всех входных данных, и входы и результаты сгруппированы в виде некоторых шаблонов, которые не пересекаются. Сюда относятся SIMD-инструкции (англ. single-instruction, multiple-data), которые запускают одновременно несколько однотипных действий над данными. Для AMD64 такие инструкции есть в расширения SSE и AVX, для ARM --- в NEON, в RISC-V --- векторные инструкции.

Межблоковые инструкции получаются из нескольких блоков графа потока управления высокоуровневого языка. Каночиным примером будет арифметика с насыщением, например max из RISC-V с расширением bitmanip.

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

Что такое порождение "оптимальных" инструкций?

Говоря про "оптимальный выбор инструкций" часто подразумевают следующее определение. Для некоторого набора I инструкций, где каждая инструкция i\in I имеет стоимость ,c_i, алгоритм выбора инструкций дает оптимальный результат, если для любой входной программы P он находит набор (с повторами) S из I такой, что S реализует P, и не существует другого такого набора S', что он тоже реализует программу P, и при этом \sum_{s' \in S'} c_{s'} < \sum_{s \in S} c_s.

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

Во-вторых, два сравнимых подхода к выбору инструкций могут породить код, который после фаз переупорядочивания инструкций и распределения регистров будет непохожего качества. Например, нам нужно породить инструкции, которые независимы друг от друга. ISA предлагает два варианта: использовать две инструкции со стоимостью 2 каждая, либо использовать одну инструкцию со стоимостью 3. Согласно критерию выше, нужно выбирать второй подход, так как там суммарная стоимость будет меньше. Но если целевая архитектура умеет исполнять несколько инструкций параллельно, то лучше первый подход.

Лекция 1 || Лекция 2 >