Распределение регистров при планировании инструкций

advertisement
Д.С. Иванов
D.S. Ivanov
РАСПРЕДЕЛЕНИЕ РЕГИСТРОВ ПРИ ПЛАНИРОВАНИИ ИНСТРУКЦИЙ ДЛЯ
АРХИТЕКТУРЫ «ЭЛЬБРУС-90МИКРО»
COMBINED REGISTER ALLOCATION AND INSTRUCTION SCHEDULING FOR
ELBRUS-90 MICROPROCESSOR ARCHITECTURE
Фазы распределения регистров и планирования инструкций в оптимизирующем компиляторе имеют много общего и выполняют схожие задачи. В статье представлен алгоритм одновременного распределения регистров и планирования инструкций, который позволяет устранить дублирующие функциональности и лучше учесть взаимные требования
указанных фаз. Эффективность алгоритма продемонстрирована на задачах пакета SPEC CINT95.
Keywords: register allocation, instruction scheduling,
phase ordering
Введение
В статье рассматривается взаимодействие таких фаз работы компилятора как планирование инструкций и распределение регистров, имеющих большое значение применительно к любой микропроцессорной архитектуре. Задача планирования инструкций состоит в определении оптимального по времени порядка их исполнения с учетом доступных архитектурных ресурсов. Распределение регистров предполагает оптимальное назначение доступных архитектурных регистров переменным и промежуточным результатам
программы и сохранение в памяти не распределенных на регистры переменных.
Когда количество доступных ресурсов заведомо превышает количество требуемых,
а число переменных в программе не превышает число доступных к распределению регистров, оптимальное решение задач планирования инструкций и распределения регистров
не представляет особых трудностей. Однако в реальности, особенно во встраиваемых системах, в которых накладываются дополнительные требования на выделяемую мощность
и размер аппаратуры, ресурсов, как правило, не хватает, поэтому от работы этих фаз во
многом зависит эффективность получаемого кода.
Регистры, по сути, являются одним из архитектурных ресурсов, которые должны
учитываться при планировании инструкций, откуда и вытекает взаимосвязь рассматриваемых фаз [1].
При разработке компилятора важно не только хорошо выбрать алгоритмы планирования инструкций и распределения регистров, но и правильно расположить эти фазы
относительно друг друга. На практике применяются различные варианты такого выбора
(т.н. phase-ordering): планирование до распределения, планирование после распределения
и распределение регистров во время планирования инструкций.
В промышленных компиляторах наиболее часто регистры распределяют до или после планирования, используя классический алгоритм Четина-Бриггса (раскраска графа
несовместимости) с дополнительными эвристиками [2]. Однако ряд исследований [1, 3, 4]
показывает, что большей эффективности – минимизации кода, уменьшения количества
обращений в память и уменьшения времени компиляции – можно добиться совмещением
фаз планирования и распределения.
1. Предыдущие исследования
Несмотря на довольно большое количество публикаций, исследования в данной
области нельзя назвать завершенными. Часто в своих работах авторы ограничиваются
адаптацией одной фазы к другой путем переноса или дублирования части функциональности.
Например, в [4] представлен алгоритм распределения регистров до планирования с
помощью раскраски расширенного графа несовместимости, в котором учитывается воз-
2
можность параллельного исполнения инструкций. Во многих случаях такая схема обеспечивает хорошее распределение регистров, однако, ей свойственен ряд недостатков. Вопервых, она не является оптимальной с точки зрения времени компиляции, т.к. в предложенном алгоритме приходится строить, а в случае нехватки регистров и перестраивать,
граф несовместимости для всех переменных программы. Построение графа несовместимости требует не только больших затрат времени, но и значительной памяти. Во-вторых,
решение авторов в первую очередь удалять дуги графа, отражающие параллелизм, тоже не
представляется оптимальным, т.к. суммарная задержка в результате отказа от параллелизма может превышать задержку, полученную в результате откачки регистра в память.
В [1], напротив, представлена адаптация фазы планирования к распределению регистров путем введения эвристики, уменьшающей размер кода откачки в память. Однако
при таком образе действий также необходимо распределять регистры, но уже после планирования, что возвращает к вопросу о времени компиляции. При доказательстве эффективности алгоритма авторы используют модель идеальной памяти с фиксированными задержками длиной один такт, что далеко не всегда соответствует действительности. Потери от появления инструкций откачки в память будут увеличиваться пропорционально величине задержек этих инструкций.
Здесь представлена попытка наиболее полно интегрировать друг в друга алгоритмы
планирования и распределения регистров с учетом как глобального, так и локального распределений.
2. Специфика микропроцессора МЦСТ R-500
Исследование проводилось при создании компилятора для встраиваемого вычислительного комплекса на основе микропроцессора МЦСТ R-500 с системой команд SPARC
v8 [5]. Использованный в компиляторе алгоритм планирования с помощью списков (List
Scheduling) был дополнен и адаптирован для параллельного распределения регистров.
3
Однако полученные результаты можно рассматривать применительно как к микропроцессорам с другой архитектурой, так и к другим алгоритмам планирования.
В зависимости от реализации, в микропроцессоре с архитектурой SPARC v8 может
быть от 40 до 520 целочисленных регистров общего назначения. Из них восемь являются
глобальными регистрами, а остальные представляют собой наборы по 16 регистров: Первые восемь регистров каждого набора называются входными, а остальные восемь – локальными. Процедуре доступны восемь глобальных регистров и 24-регистровое окно, которое, помимо входных и локальных регистров, включает восемь выходных регистров,
являющихся входными для смежного регистрового окна.
Микропроцессор МЦСТ R-500 способен выполнять по одной инструкции за такт,
поэтому там, где это ограничение существенно, будет представлено соответствующее решение для машин, выполняющих за такт более одной инструкции.
3. Общее описание алгоритма
Для описания переменных программы введем понятие символических или виртуальных регистров [2] в противоположность реальным архитектурным регистрам, которые
в дальнейшем будут называться физическими. Таким образом, можно говорить о задаче
распределения регистров как об оптимальном отображении множества виртуальных регистров на множество регистров физических. Каждому виртуальному регистру поставим в
соответствие сеть, которая включает множество узлов и дуг управляющего графа процедуры, на котором виртуальный регистр должен хранить значение соответствующей ему
переменной (в дальнейшем для краткости будем говорить, что регистр или сеть живет в
данной точке программы) для корректного исполнения программы. Запись в регистр, соответствующий данной сети, будем называть определением сети, а чтение такого регистра
– использованием сети. Сети, живущие в пределах узла управляющего графа процедуры
(линейного участка), будем называть локальными, остальные сети – глобальными.
4
Построение сетей и определение их свойств необходимо выполнить до начала планирования. Набор свойств зависит от реализации алгоритма и от архитектуры, однако, как
правило, необходимо хранить информацию о количестве использований и определений
сети, тип сети (например, целочисленные и плавающие сети), формат сети (при наличии
регистров различного формата), счетчик исполнения сети (сумма счетчиков исполнения
всех использований и определений) и т.д.
Если сеть на всем времени жизни ни разу не была откачана в память, ей должен соответствовать один физический регистр. Поддержка этого свойства для глобальных сетей
во время планирования затруднительна и особого смысла не имеет. Поэтому множество
глобальных сетей, которые будут распределены на регистры, известно уже после построения информации о сетях, и распределение регистров для них следует выполнять до планирования. Глобальные сети, которым не хватило регистров, будут откачиваться в память по
мере планирования их определений и использований.
Основным фактором при раздельном распределении локальных и глобальных сетей
является правильный баланс между количеством регистров, выделяемых для глобального
и для локального распределений в каждом узле управляющего графа. Т.к. количество одновременно живых локальных сетей заранее неизвестно, для определения этого баланса
применяются эвристики, основанные на свойствах графа зависимостей линейного участка.
При разработке эвристики необходимо учесть, что, как правило, не все локальные
сети живут одновременно. Часто одну локальную сеть можно начать планировать после
завершения планирования другой, используя один и тот же регистр. Количество одновременно живых локальных сетей можно довольно точно оценить, исходя из степени параллельности графа зависимостей. Пусть среднее арифметическое времен раннего и позднего
планирования [6] первого определения локальной сети i есть TD(i), а среднее арифметическое соответствующих времен планирования последнего использования – TU(i). Допустим, что локальные сети равномерно распределены по всему узлу, тогда количество од-
5
новременно живых локальных сетей можно оценить по формуле:
LWN =
 (TU (i)  TD(i)) ,
H
где H – высота линейного участка, т.е. разница между временами планирования его конца
и начала.
После распределения регистров для глобальных сетей запускается планирование с
одновременным распределением регистров для локальных сетей и тех глобальных сетей,
которым не хватило регистров при глобальном распределении. В случае дефицита регистров алгоритм планирования перестает учитывать время позднего планирования инструкции и работает по указаниям распределителя регистров (табл. 1).
Таблица 1
Планирование с учетом давления на регистры
Порядок инструкций
в промежуточном
представлении
MOV const1 -> vr1
STORE vr1
MOV const2 -> vr2
STORE vr2
MOV const3 -> vr3
STORE vr3
FLOAD const_addr1 -> vr4
FADD vr4, const -> vr5
FMUL vr5, const -> vr6
FSTORE const_addr2 vr6
Планирование по времени
позднего инструкций
Планирование с учетом
давления на регистры
FLOAD const_addr -> vr4 (r1)
MOV const -> vr1 (r2)
FADD vr4 (r1), const -> vr5 (r1)
MOV const -> vr2 (r3)
FMUL vr5 (r1), const -> vr6 (r1)
MOV const -> vr3 (r4)
FSTORE const_addr vr6 (r1)
STORE vr1 (r2)
STORE vr2 (r3)
STORE vr3 (r4)
FLOAD const_addr -> vr4 (r1)
MOV const -> vr1 (r2)
FADD vr4 (r1), const -> vr5 (r1)
STORE vr1 (r2)
FMUL vr5 (r1), const -> vr6 (r1)
MOV const -> vr2 ( r2)
FSTORE const_addr vr6 (r1)
MOV const -> vr3 (r1)
STORE vr2 (r2)
STORE vr3 (r1)
Объединение фаз планирования инструкций и распределения регистров дает ряд
преимуществ, таких как уменьшение сложности алгоритмов, уменьшение размера получаемого кода, нивелирование лишних задержек и динамический контроль количества инструкций откачки в память. Рассмотрим их более подробно.
4. Уменьшение алгоритмической сложности
6
Классический алгоритм распределения регистров путем раскраски графа состоит в
построении графа несовместимости и его, вообще говоря, итеративной раскраске R цветами, где R – количество доступных регистров [7].
В распределении регистров для глобальных сетей перед планированием используется более простой аналог графа несовместимости. Архитектурному регистровому файлу
соответствует битовая матрица размером K×R, где R – количество доступных физических
регистров, K – количество узлов управляющего графа, а каждой сети соответствует битовый вектор, в котором каждый бит соответствует узлу управляющего графа. Единица в i-й
позиции вектора означает, что сеть жива на входе или выходе узла или живет в этом узле.
Сеть можно распределить на регистр n, если конъюнкция n-го столбца матрицы и битового вектора сети дает нулевой вектор. Если результат конъюнкции – ненулевой вектор,
необходимо провести дополнительный анализ для каждого ненулевого бита. Распределение двух сетей на один регистр в узле возможно, когда одна сеть не является живой в момент определения другой сети [2]. Для этого необходимо и достаточно, чтобы обе сети не
имели определений в рассматриваемом узле, либо чтобы одновременно выполнялись следующие условия:
1) первая сеть жива только на входе узла (время жизни заканчивается в узле);
2) вторая сеть жива только на выходе из узла (время жизни начинается в узле);
3) инструкция, заканчивающая время жизни первой сети, может быть спланирована раньше инструкции, начинающей время жизни второй сети, либо эти инструкции совпадают.
Если дополнительный анализ дает положительный результат для всех ненулевых
битов, то две сети могут быть распределены на один регистр.
Вместо битовой матрицы можно также использовать расширенный граф несовместимости [4]. Обозначим A(N) – сложность алгоритма раскраски графа несовместимости,
A(N) = O(N 2), где N – количество сетей в программе, по сути – количество узлов в графе.
7
Пусть G – количество глобальных сетей, а L – количество локальных, N = G + L. Тогда
сложность алгоритма распределения регистров при планировании можно записать как
A(G) + B(L), где B – сложность алгоритма распределения регистров для локальных сетей.
Оценим B(L). Регистры локальным сетям и сетям, не попавшим в память, распределяются
динамически, поэтому основной проблемой является поиск свободного регистра путем
обхода регистрового файла. Время поиска свободного регистра зависит от размера регистрового файла R и количества одновременно живых в данный момент сетей n (чем больше сетей, тем больше регистров занято, тем меньше вероятность найти свободный регистр
с первой попытки). Таким образом:
B( L) =  n  l  R  (G + L / K )  l  R =(G + L / K )  L  R ,
следовательно, сложность нового алгоритма не превышает:
G 2 + G  L  R + L2  R / K .
Число локальных сетей в линейном участке в различных программах варьируется в
небольших диапазонах, поэтому L / K ~ c, где c – некая константа (как правило, с<<L), откуда сложность алгоритма будет равна:
G 2 + G  L  R + c  L  R = G 2 + L  R  (G + c) .
По сравнению с раскраской графа несовместимости в этом выражении отсутствует
наиболее значимый член L2.
5. Учет регистров как архитектурного ресурса
Без построения времен жизни алгоритм планирования не может точно знать количество живых в данный момент переменных, что затрудняет хорошее, с точки зрения распределителя регистров, планирование, которое описано в [3]. При одновременном планировании и распределении регистров такая информация доступна, поэтому, начиная с некоторого критического давления на регистры, можно откладывать инструкции, стоящие
на критическом пути, в пользу инструкций, уменьшающих давление на регистровый файл.
8
Такая схема хорошо работает для машин, выполняющих по одной инструкции за
такт, т.к. одного прохода по списку готовых к планированию инструкций достаточно для
определения наиболее подходящей инструкции. В качестве альтернативы для параллельных машин можно использовать другую модель планирования по критическому давлению
на регистры: откладывать тяжелые инструкции лишь в том случае, когда регистров достоверно не хватает и в списке готовых есть инструкция, уменьшающая давление на регистры. При таком подходе растягивание кода оказывается оправданным, т.к. оно заведомо
позволяет избежать появления инструкций откачки в память. Помимо списка готовых
можно также просматривать и список ждущих инструкций.
6. Оптимизация задержек при обращении в память
При недостатке регистров некоторые сети приходится хранить в памяти. В RISCархитектурах вообще и в R-500, в частности, работа с памятью осуществляется только через инструкции записи в память (store) и чтения из памяти (load), а все вычисления производятся на регистрах.
Скорость выполнения чтения из памяти зависит от того, на каком уровне иерархии
памяти находятся запрашиваемые данные. Для кэш-памяти первого уровня это 1-2 такта,
для второго уровня – 3-5 тактов, для основной памяти – 12-55 тактов [8, 9].
При планировании инструкций чтения из памяти обычно делается оптимистичное
предположение о нахождении данных в кэш-памяти первого уровня, т.е. задержка составляет 1-3 такта. Если задержка составляет более одного такта, то помещение инструкций
чтения из памяти в спланированный код приводит к дополнительным трудностям. Вставка
инструкции чтения непосредственно перед использованием данных приводит к потере одного такта. Вставка инструкции чтения выше, с учетом задержки, по ряду причин требует
проведения дополнительного анализа. Например, регистр, в который подкачиваются данные, может использоваться предыдущей инструкцией.
9
Аналогично, результат инструкции можно записать в память только после ее завершения. Но вставка инструкций записи в память после планирования приведет к потере
тактов, т.к. выполнение некоторых арифметических инструкций (умножение, деление)
может составлять значительное время или потребует дополнительного анализа для переноса инструкции записи на несколько тактов вниз.
Одновременное планирование инструкций и распределение регистров позволяет
выдержать необходимые задержки естественным образом, благодаря созданию зависимостей, обозначающих связь исходных инструкций с инструкциями записи в память, которые учитываются алгоритмом планирования. Таким образом, инструкции записи в память
участвуют в планировании на равных правах с остальными инструкциями.
Нужно заметить, что учет задержек не так актуален в out-of-order архитектурах, в
которых при необходимости инструкции могут быть переставлены аппаратурой.
7. Откачка в память констант и удаление ненужных инструкций
При недостатке регистров не все переменные выгодно откачивать в память.
Например, для сетей, имеющих только одно определение – запись константы в регистр
(сети, обладающие таким свойством, будем называть константными), гораздо лучшим решением будет дублирование определения перед каждым использованием (табл. 2).
Таблица 2
Оптимизация константных сетей
До распределения
MOV const -> vr2
Распределение с
откачкой констант
в память
MOV const -> r2
MEMORY <- r2
Необходима откачка
регистра в память
STORE vr2
Распределение без
Распределение без откачки констант в
откачки констант в память с удалением
память
ненужных инструкций
MOV const -> r2
Дублирование MOV
MEMORY-> r3
STORE r3
MOV const -> r3
STORE r3
Лишняя инструкция,
результат
которой
нигде не используется, удалена
MOV const -> r3
STORE r3
10
При этом начальное определение может оказаться ненужным, если:
1) в узле с определением нет ни одного использования;
2) в узле с определением регистра есть использования, однако необходимость в
переиспользовании регистра возникла между планированием определения и первого использования.
В архитектуре микропроцессора МЦСТ R-500 есть два типа константных определений. Это инструкция пересылки MOV, дающая по сравнению с откачкой сети в память
выигрыш минимум в один такт для каждого использования, и пара инструкций загрузки
32-битной константы SETHI + OR. На задачах пакета spec95 замена инструкций LOAD
парами SETHI + OR приводит к небольшой потере производительности, т.к. данные небольших программ умещаются в кэш-памяти первого уровня. Этот экспериментальный
факт косвенно подтверждает правильность предположения о нахождении данных в кэшпамяти первого уровня.
8. Результаты
Результаты сравнения одновременного планирования и распределения регистров и
распределения регистров методом Четина-Бриггза после планирования инструкций приведены на рис. 1 и 2.
Рисунок 1 демонстрирует изменение производительности на тестах spec95. Улучшение производительности получено за счет уменьшения количества обращений в память,
учета задержек и минимизации количества пересылок. На тестах 124.m88ksim и 130.li
улучшения производительности не наблюдается. Это связано с тем, что наиболее часто
вызываемые процедуры этих тестов имеют небольшой размер и не требуют откачки данных в память.
11
Увеличение производительности
1,10
1,08
1,07
1,06
1,06
1,05
1,04
1,03
1,03
1,02
1,00
1,00
1,00
14
7.
vo
rte
x
13
2.
i jp
eg
13
0.
li
es
s
12
9.
co
m
pr
12
6.
gc
c
88
ks
im
12
4.
m
09
9.
go
0,98
Рис. 1
Увеличение производительности на задачах SPEC CINT95
Уменьшение времени компиляции
2,10
2,06
1,99
1,90
1,76
1,62
1,70
1,50
1,33
1,29
1,30
1,10
1,10
0,90
0,70
14
7.
vo
rte
x
13
2.
i jp
eg
13
0.
li
es
s
12
9.
co
m
pr
12
6.
gc
c
88
ks
im
12
4.
m
09
9.
go
0,50
Рис. 2
Уменьшение временных затрат на фазы планирования инструкций и распределения
регистров на задачах SPEC CINT95
12
На рисунке 2 показано отношение суммарного времени, затраченного на планирование и следующее за ним распределение регистров, ко времени работы одновременного
планирования и распределения регистров. Уменьшение времени компиляции наблюдается
на всех тестах, но его величина существенно зависит от особенностей задачи.
9. Выводы
Объединение распределения регистров и планирования инструкций в рамках одной
фазы позволяет значительно уменьшить количество обращений в память, а, значит, и снизить время исполнения программы. Разделение распределения локальных и глобальных
сетей уменьшает алгоритмическую сложность по сравнению с распределением через раскраску графа несовместимости.
Представленный алгоритм был реализован в составе промышленного компилятора
для вычислительных комплексов «Эльбрус-90микро» на базе микропроцессора МЦСТ R500 с архитектурой SPARC v8. Планируется разработка и реализация алгоритма для микропроцессоров серии «Эльбрус» с архитектурой широкого командного слова.
Литература
1. R. Motwani, K.V. Palem, V. Sarkar, S. Reyen. Combining Register Allocation and Instruction Scheduling (CRISP): Technical Report TR 698 / Courant Institute, July 1995.
2. Steven S. Muchnik. Advanced Compiler Design and Implementation, Morgan Kauffman, San Francisco, 1997.
3. J.R. Goodman, W. Hsu. Code Scheduling and Register Allocation in Large Basic
Blocks // in Proceedings of the 2nd international conference on Supercomputing, 1988, p. 442–
452.
4. S.S. Pinter. Register Allocation with Instruction Scheduling: a New Approach //ACM
SIGPLAN Notices, Volume 28, Issue 6, June, 1993, p. 248–257.
13
5. The SPARC Architecture Manual Version 8, SPARC International Inc., 1992.
6. R. Allen and K. Kennedy. Optimizing Compilers for Modern Architectures, Morgan
Kauffman, San Francisco, 2002.
7. Chaitin, G.J. Register allocation & spilling via graph coloring. // in Proceedings of the
1982 SIGPLAN symposium on Compiler construction, 1982, p. 98 – 105.
8. В. Корнеев, А. Киселев. Современные микропроцессоры, СПб., БХВ-Петербург,
2003.
9. Hennessy J., Patterson D. Computer Architecture: A Quantitative Approach. Morgan
Kauffman, 2003.
14
Download