В дизайне компилятора статическая форма одиночного присваивания (часто сокращенно SSA-форма или просто SSA ) — это тип промежуточного представления (IR) , где каждая переменная присваивается ровно один раз. SSA используется в большинстве высококачественных оптимизирующих компиляторов для императивных языков, включая LLVM , GNU Compiler Collection и многие коммерческие компиляторы.
Существуют эффективные алгоритмы преобразования программ в форму SSA. Для преобразования в SSA существующие переменные в исходном IR разделяются на версии, новые переменные обычно обозначаются исходным именем с индексом, так что каждое определение получает свою собственную версию. Дополнительные операторы, которые назначаются новым версиям переменных, также могут потребоваться для введения в точке соединения двух путей потока управления. Преобразование из формы SSA в машинный код также эффективно.
SSA упрощает выполнение многочисленных анализов, необходимых для оптимизации, таких как определение цепочек use-define , поскольку при рассмотрении использования переменной есть только одно место, где эта переменная могла получить значение. Большинство оптимизаций можно адаптировать для сохранения формы SSA, так что одна оптимизация может выполняться за другой без дополнительного анализа. Оптимизации на основе SSA обычно более эффективны и мощны, чем их предыдущие эквиваленты без формы SSA.
В компиляторах функциональных языков , таких как для Scheme и ML , обычно используется стиль продолжения-передачи (CPS). SSA формально эквивалентен хорошо работающему подмножеству CPS, исключая нелокальный поток управления, поэтому оптимизации и преобразования, сформулированные в терминах одного, обычно применяются к другому. Использование CPS в качестве промежуточного представления более естественно для функций высшего порядка и межпроцедурного анализа. CPS также легко кодирует call/cc , тогда как SSA этого не делает. [1]
SSA была разработана в 1980-х годах несколькими исследователями из IBM . Кеннет Задек, ключевой член команды, перешел в Университет Брауна по мере продолжения разработки. [2] [3] В статье 1986 года были представлены точки рождения, назначения идентичности и переименование переменных таким образом, чтобы переменные имели единственное статическое назначение. [4] Последующая статья 1987 года Жанны Ферранте и Рональда Ситрона [5] доказала, что переименование, сделанное в предыдущей статье, устраняет все ложные зависимости для скаляров. [3] В 1988 году Барри Розен, Марк Н. Вегман и Кеннет Задек заменили назначения идентичности на Φ-функции, ввели название «статическая форма с одним назначением» и продемонстрировали ныне распространенную оптимизацию SSA. [6] Название Φ-функция было выбрано Розеном как более публикуемая версия «фальшивой функции». [3] Альперн, Вегман и Задек представили другую оптимизацию, но используя название «статическое одиночное присваивание». [7] Наконец, в 1989 году Розен, Вегман, Задек, Ситрон и Ферранте нашли эффективный способ преобразования программ в форму SSA. [8]
Основная польза SSA заключается в том, что он одновременно упрощает и улучшает результаты различных оптимизаций компилятора , упрощая свойства переменных. Например, рассмотрим этот фрагмент кода:
у := 1у := 2х := у
Люди могут видеть, что первое назначение не является необходимым, и что ценность y
использования в третьей строке исходит из второго назначения y
. Программа должна была бы выполнить анализ определения достижения, чтобы определить это. Но если программа находится в форме SSA, оба эти действия выполняются немедленно:
у 1 := 1у 2 := 2х 1 := у 2
Алгоритмы оптимизации компилятора , которые либо становятся доступными, либо значительно улучшаются благодаря использованию SSA, включают:
a=3*4+5;
так, как если бы она былаa=17;
Преобразование обычного кода в форму SSA в первую очередь заключается в замене цели каждого назначения новой переменной и замене каждого использования переменной на «версию» переменной, достигшей этой точки. Например, рассмотрим следующий график потока управления :
Изменение имени слева от "x x - 3" и изменение последующих использований x на это новое имя оставит программу без изменений. Это можно использовать в SSA, создав две новые переменные: x 1 и x 2 , каждая из которых назначается только один раз. Аналогично, присваивание отличительных индексов всем остальным переменным дает:
Понятно, к какому определению относится каждое использование, за исключением одного случая: оба использования y в нижнем блоке могут относиться либо к y 1 , либо к y 2 , в зависимости от того, по какому пути пойдет поток управления.
Чтобы решить эту проблему, в последний блок вставляется специальный оператор, называемый функцией Φ (Phi) . Этот оператор сгенерирует новое определение y, называемое y 3 , «выбирая» либо y 1 , либо y 2 , в зависимости от потока управления в прошлом.
Теперь последний блок может просто использовать y 3 , и правильное значение будет получено в любом случае. Функция Φ для x не нужна: только одна версия x , а именно x 2 , достигает этого места, так что проблемы нет (другими словами, Φ( x 2 , x 2 )= x 2 ).
Учитывая произвольный граф потока управления, может быть сложно сказать, куда вставлять функции Φ и для каких переменных. Этот общий вопрос имеет эффективное решение, которое можно вычислить с использованием концепции, называемой границами доминирования (см. ниже).
Функции Φ не реализованы как машинные операции на большинстве машин. Компилятор может реализовать функцию Φ, вставив операции «перемещения» в конец каждого предшествующего блока. В приведенном выше примере компилятор может вставить перемещение из y 1 в y 3 в конец среднего левого блока и перемещение из y 2 в y 3 в конец среднего правого блока. Эти операции перемещения могут не попасть в конечный код на основе процедуры распределения регистров компилятора . Однако этот подход может не работать, когда одновременные операции спекулятивно производят входные данные для функции Φ, как это может произойти на машинах с широким выпуском . Обычно машина с широким выпуском имеет инструкцию выбора, используемую в таких ситуациях компилятором для реализации функции Φ.
В графе потока управления узел A считается строго доминирующим над другим узлом B, если невозможно достичь B, не пройдя сначала через A. Другими словами, если достигнут узел B, то можно предположить, что A выполнилось. Говорят, что A доминирует над B (или B доминируется A), если либо A строго доминирует над B, либо A = B.
Узел, который передает управление узлу A, называется непосредственным предшественником A.
Граница доминирования узла A — это набор узлов B, где A не доминирует строго над B, но доминирует над некоторым непосредственным предшественником B. Это точки, в которых несколько путей управления снова сливаются в один путь.
Например, в следующем коде:
[1] x = случайный()если х < 0,5 [2] результат = "орел"еще [3] результат = "решка"конец[4] печать(результат)
Узел 1 строго доминирует над 2, 3 и 4, а непосредственными предшественниками узла 4 являются узлы 2 и 3.
Границы доминирования определяют точки, в которых необходимы функции Φ. В приведенном выше примере, когда управление передается узлу 4, определение used result
зависит от того, было ли управление передано из узла 2 или 3. Функции Φ не нужны для переменных, определенных в доминаторе, поскольку существует только одно возможное определение, которое может применяться.
Существует эффективный алгоритм для поиска границ доминирования каждого узла. Этот алгоритм был первоначально описан в "Efficiently Computing Static Single Assignment Form and the Control Graph" Рона Ситрона, Жанны Ферранте и др. в 1991 году. [10]
Кит Д. Купер, Тимоти Дж. Харви и Кен Кеннеди из Университета Райса описывают алгоритм в своей статье под названием «Простой, быстрый алгоритм доминирования» : [11]
для каждого узла b граница_доминирования(б) := {}для каждого узла b , если число непосредственных предшественников b ≥ 2 для каждого p в непосредственных предшественниках b бегун := p пока бегун ≠ idom(b) доминирование_граница(бегун) := доминирование_граница(бегун) ∪ { b } бегун := idom(бегун)
В приведенном выше коде idom(b)
— непосредственный доминант b, уникальный узел, который строго доминирует над b, но не доминирует строго над любым другим узлом, который строго доминирует над b.
«Минимальный» SSA вставляет минимальное количество функций Φ, необходимое для того, чтобы гарантировать, что каждому имени присвоено значение ровно один раз и что каждая ссылка (использование) имени в исходной программе может по-прежнему ссылаться на уникальное имя. (Последнее требование необходимо для того, чтобы гарантировать, что компилятор может записать имя для каждого операнда в каждой операции.)
Однако некоторые из этих функций Φ могут быть мертвыми . По этой причине минимальный SSA не обязательно производит наименьшее количество функций Φ, необходимых для определенной процедуры. Для некоторых типов анализа эти функции Φ являются излишними и могут привести к тому, что анализ будет выполняться менее эффективно.
Сокращенная форма SSA основана на простом наблюдении: функции Φ нужны только для переменных, которые являются «живыми» после функции Φ. (Здесь «живыми» означает, что значение используется на некотором пути, который начинается с рассматриваемой функции Φ.) Если переменная не является живой, результат функции Φ не может быть использован, и присвоение функцией Φ недействительно.
Построение сокращенной формы SSA использует информацию о живой переменной в фазе вставки функции Φ для принятия решения о необходимости данной функции Φ. Если исходное имя переменной не является живой в точке вставки функции Φ, функция Φ не вставляется.
Другая возможность — рассматривать обрезку как проблему устранения мертвого кода . Тогда функция Φ является живой, только если любое использование во входной программе будет перезаписано в нее, или если она будет использована в качестве аргумента в другой функции Φ. При вводе формы SSA каждое использование перезаписывается в ближайшее определение, которое доминирует над ней. Функция Φ будет затем считаться живой, пока это ближайшее определение, которое доминирует хотя бы над одним использованием или хотя бы над одним аргументом живой Φ.
Полуобрезанная форма SSA [12] — это попытка сократить количество функций Φ без относительно высокой стоимости вычисления информации о живых переменных. Она основана на следующем наблюдении: если переменная никогда не является живой при входе в базовый блок, ей никогда не понадобится функция Φ. Во время построения SSA функции Φ для любых «локальных для блока» переменных опускаются.
Вычисление набора локальных переменных блока является более простой и быстрой процедурой, чем полный анализ живых переменных, что делает полуобрезанную форму SSA более эффективной для вычисления, чем обрезанную форму SSA. С другой стороны, полуобрезанная форма SSA будет содержать больше функций Φ.
Аргументы блока являются альтернативой функциям Φ, которая по представлению идентична, но на практике может быть более удобной во время оптимизации. Блоки именуются и принимают список аргументов блока, обозначенных как параметры функции. При вызове блока аргументы блока привязываются к указанным значениям. MLton , Swift SIL и LLVM MLIR используют аргументы блока. [13]
Форма SSA обычно не используется для прямого выполнения (хотя можно интерпретировать SSA [14] ), и она часто используется «поверх» другого IR, с которым она остается в прямом соответствии. Это может быть достигнуто путем «построения» SSA как набора функций, которые отображают части существующего IR (базовые блоки, инструкции, операнды и т. д. ) и его аналог SSA. Когда форма SSA больше не нужна, эти функции отображения могут быть отброшены, оставив только оптимизированный IR.
Выполнение оптимизаций в форме SSA обычно приводит к запутанным SSA-сетям, то есть существуют инструкции Φ, операнды которых не все имеют одинаковый корневой операнд. В таких случаях для выхода из SSA используются алгоритмы раскрашивания . Наивные алгоритмы вводят копию вдоль каждого предшествующего пути, что приводит к тому, что источник корневого символа, отличного от назначения Φ, помещается в Φ. Существует несколько алгоритмов для выхода из SSA с меньшим количеством копий, большинство из них используют интерференционные графы или некоторую их аппроксимацию для объединения копий. [15]
Расширения формы SSA можно разделить на две категории.
Расширения схемы переименования изменяют критерий переименования. Напомним, что форма SSA переименовывает каждую переменную, когда ей присваивается значение. Альтернативные схемы включают статическую форму одноразового использования (которая переименовывает каждую переменную в каждом операторе, когда она используется) и статическую форму одноразовой информации (которая переименовывает каждую переменную, когда ей присваивается значение, и на границе постдоминирования).
Расширения , специфичные для функций , сохраняют свойство единственного назначения для переменных, но включают новую семантику для моделирования дополнительных функций. Некоторые расширения, специфичные для функций, моделируют высокоуровневые функции языка программирования, такие как массивы, объекты и псевдонимные указатели. Другие расширения, специфичные для функций, моделируют низкоуровневые архитектурные функции, такие как спекуляция и предикация.
{{cite book}}
: CS1 maint: отсутствует местоположение издателя ( ссылка ){{cite journal}}
: Цитировать журнал требует |journal=
( помощь )