Задача глобального распределения регистров во время динамической двоичной трансляции
К.А. Батузов <[email protected]> Институт системного программирования РАН, 109004, Россия, г. Москва, ул. А. Солженицына, д. 25
Аннотация. Распределение регистров оказывает существенное влияние на производительность генерируемого кода. В данной статье исследуется задача распределения регистров во время динамической двоичной трансляции. Так как существующие алгоритмы распределения регистров рассчитаны на использование в компиляторах, они плохо подходят для использования во время динамической двоичной трансляции. Был разработан новый алгоритм, который определяет, какие переменные должны располагаться на каких регистрах в начале и в конце базового блока (назовем эти ограничения пред- и постусловиями данного базового блока), а затем решает задачу локального распределения регистров в данных ограничениях. Для обеспечения корректности ограничений алгоритм должен работать так, чтобы бля любого базового блока Ь', предшествующего блоку Ь, постусловия блока Ь' совпадали с предусловиями блока Ь. Этого можно достичь, если выделить в графе потока управления группы дуг, на всех концах которых ограничения должны быть неизменны. Такие дуги называются точками синхронизации. Точки синхронизации являются связными компонентами в неориентированном графе, вершинами которого являются дуги графа потока управления, а ребрами соединены те дуги-вершины, которые либо входят, либо выходят из одного базового блока. Данное утверждение позволяет эффективно находить точки синхронизации. Для определения того, сколько переменных должно находиться на регистрах в точке синхронизации, количество доступных регистров оценивается с помощью регистрового давления. Затем выбираются конкретные переменные на их частоты использования в данном фрагменте кода. Алгоритм локального распределения регистров был модифицирован, чтобы использовать предусловия и обеспечивать постусловия. В статье приводится эффективный алгоритм для приведения существующего распределения в конце базового блока к требуемым постусловиям и доказывается оптимальность этого алгоритма. Применение описанного алгоритма распределения регистров привело к сокращению времени работы синтетического примера на 29.6%.
Ключевые слова: глобальное распределение регистров; динамическая двоичная трансляция; эмуляторы; (^ЕМи.
DOI: 10.15514/ISPRAS-2016-28(5)-12
Для цитирования: K.A. Батузов. Задача глобального распределения регистров во время динамической двоичной трансляции. Труды ИСП РАН, том 28, вып. 5, 2016, стр. 199-214. DOI: 10.15514/ISPRAS-2016-28(5)-12
1. Введение
При разработке виртуальных машин возникает задача выполнения кода, написанного для одной процессорной архитектуры, на другой архитектуре. Для ее решения используется динамическая двоичная трансляция. Трансляция кода ведется небольшими фрагментами, которые называются блоками трансляции. Типичная реализация динамической двоичной трансляции предполагает сначала дизассемблирование блока трансляции в некоторое внутреннее представление, а затем генерацию машинного кода для целевой архитектуры из этого внутреннего представления. На втором этапе возникает необходимость распределения регистров. Данная статья посвящена вопросу решения задачи глобального распределения регистров во время динамической трансляции. Распределение регистров оказывает существенное влияние на производительность полученного кода. При этом необходимо учитывать, что в случае динамической двоичной трансляции распределение регистров происходит во время работы программы, и время работы самого алгоритма должно быть как можно меньшим. Локальное распределение регистров может быть реализовано очень быстрым жадным алгоритмом [1], однако глобальное распределение регистров может давать более эффективный результирующий код. Существующие алгоритмы глобального распределения регистров (алгоритмы раскраски графов зависимостей [2], алгоритмы линейного сканирования [3]) не вполне учитывают ограничения на накладные расходы для выполнения самого алгоритма. Поэтому был разработан новый алгоритм, который выполняет глобальное распределение регистров с очень низкими накладными расходами. Данный алгоритм учитывает особенности кода, возникающего во время динамической двоичной трансляции. В своей работе он использует локальный алгоритм для распределения регистров внутри базового блока.
Полученный алгоритм был реализован в эмуляторе QEMU [4]. Этот эмулятор был выбран, так как он обладает открытым исходным кодом, а также широко применяется на практике.
В качестве внутреннего представления в QEMU используется последовательность инструкций, оперирующих с переменными трех видов и константами. Инструкции представляют собой язык ассемблера упрощенной абстрактной машины и содержат арифметические и логические операции, операции установки меток, условного и безусловного перехода на метку, вызова функций, загрузки из памяти и сохранения в память. Переменные внутреннего представления делятся на три вида: глобальные, которые существуют все время работы эмулятора, локальные, которые существуют в 200
рамках одного блока трансляции, и временные, которые существуют в рамках одного базового блока. Вид переменной явно указывается при ее создании. В <ЗЕМи распределение регистров объединено с генерацией кода. Вариант промежуточного представления, в котором регистры были бы частично распределены, отсутствует. Кроме того, при разработке алгоритма время его работы является очень критичным, так как он выполняется во время выполнения программы. Таким образом, при введении любого нового внутреннего представления требуется учитывать необходимые накладные расходы на создание данного представления.
Алгоритмы глобального распределения регистров, основанные на раскраске графа зависимостей, плохо решают поставленную задачу, так как они имеют непредсказуемое время работы, что недопустимо в случае динамической двоичной трансляции. Алгоритмы линейного сканирования требуют введение еще одного полноценного внутреннего представления с распределенными регистрами, так как им требуется второй проход для устранения противоречий на границах базовых блоков.
Построим новый алгоритм, который будет минимизировать расходы на дополнительное внутреннее представление, но перед этим отметим одну особенность промежуточного представления, получаемого при динамической двоичной трансляции.
При трансляции гостевого кода во внутреннее представление каждая инструкция переводится в последовательность команд внутреннего представления. Все входные аргументы эта последовательность команд получает на глобальных переменных (соответствующих регистрам гостевой архитектуры) либо в памяти. Выходные данные располагаются там же. Все же остальные переменные живы только внутри таких последовательностей и используются для хранения результатов промежуточных вычислений. Таким образом, количество переменных, интервалы жизни которых пересекают границы базовых блоков, существенно меньше общего числа переменных. Алгоритм, описанный в статье [1], может быстро и эффективно распределять регистры в рамках одного базового блока. Не составляет труда немного модифицировать его так, чтобы он принимал во внимание условия на границах базового блока: какие глобальные переменные должны находиться на каких регистрах. Значит, можно построить алгоритм, являющийся комбинацией глобального и локального распределения регистров. Часть, отвечающая за глобальное распределение регистров, будет работать только с глобальными переменными и устанавливать условия на границах базовых блоков. Часть, отвечающая за локальное распределение регистров, будет осуществлять распределение регистров внутри базовых блоков с учетом условий на их границах. Условия на границах блока трансляции будут иметь вид множества пар (V, г), означающих что переменная V должна в данной точке находиться на регистре г. Если переменная не фигурирует ни в одной из пар, то считается что переменная должна располагаться в памяти.
2. Схема комбинированного алгоритма
Назовем условия на распределение регистров в начале базового блока начальными, а в конце — конечными, а совокупность начальных и конечных условий — граничными. Введем следующие обозначения.
• Если есть базовый блок Ь, то начальные условия этого базового блока обозначим Ьрге, а конечные — как bpost.
• Все множество начальных условий для всех базовых блоков блока трансляции ТВ с графом потока управления G = (В, Е) обозначим как Срге, конечных — cpost, а всех граничных условий — С.
Множество граничных условий С блока трансляции ТВ с графом потока управления G = (В, Е) назовем корректным, если V(b1,b2) £ Е: b^ost = Ь2ге. Дуги ехи е2 графа потока управления назовем родственными, если они выходят из одного и того же блока, либо входят в один и тот же блок. Обозначим ех ~ е2-
Точкой синхронизации назовем не пустое множество J дуг графа потока управления, такое что
• для любых двух родственных дуг ег и е2 выполнено соотношение ех 6 J фф е2 6 J,
• для любых двух дуг и и V графа потока управления входящих в одну точку синхронизации существует последовательность дуг е1е2/..,ек таких, что е1 = u, ek = v, Vi 6 [1,к — l]e¡ ~ ei+1.
Утверждение Пусть дуги ег = (u^v-J и е2 = (u2 v2) графа потока управления принадлежат одной и той же точке синхронизации J. Тогда для любого
Ч f-л post
корректного множества граничных условии С выполнены равенства и^ =
post pre pre и2 И vp =vp .
Это утверждение может быть легко доказано от противного на основе определения точки синхронизации.
Данное утверждение, являющееся необходимым условием корректности множества граничных условий, послужит основой для комбинированного алгоритма: необходимо для каждой точки синхронизации выбрать граничные условия, которые будут записаны на обоих концах дуг, входящих в эту точку синхронизации.
Пусть дан блок трансляции ТВ с графом потока управления G = (В, Е). Построим неориентированный граф GE = (Е, F), в котором (е^е^ 6 F тогда и только тогда, когда ег ~ е2.
Утверждение Множество дуг {ej графа потока управления G = (В, Е) является точкой синхронизации тогда и только тогда, когда они образуют компоненту связности в графе GE.
Доказательство этого утверждения следует из определений точки синхронизации и компоненты связности графа.
У данного утверждения есть ряд важных следствий.
1. Любые две точки синхронизации либо совпадают, либо не пересекаются.
2. Каждая дуга принадлежит ровно одной точке синхронизации.
3. Эффективно находить и обходить точки синхронизации можно с помощью поиска в ширину, либо поиска в глубину в графе GE.
Далее приведем псевдокод алгоритма распределения регистров и докажем его корректность. Алгоритм будет считать использовать множество точек синхронизации и функцию для вычисления требуемых граничных условий в точке синхронизации Compute -Register-Mapping(J). Реализация данной функции будет рассмотрена позже.
Все множество точек синхронизации блока трансляции ТВ обозначим J(TB), множество всех базовых блоков, входящих в его поток управления — В(ТВ).
procedure Combined-Reg-Alloc(TB) for all b 6 В (ТВ) do bpre 0 bpost 0
end for
for all J 6 J (ТВ) do
regmap <-Compute-Register-Mapping(J) for all (src.dst) 6 J do
srcpost regmap
dstpre <- regmap
end for
end for end procedure
Утверждение Множество граничных условий, полученное в ходе работы комбинированного алгоритма распределение регистров, корректно. Методом доказательства от противного легко показать, что для любого блока b его предусловия (и, аналогично, постусловия) не могут быть установлены при обработке двух различных точек синхронизации. Далее, из того, что при обработке одной точки синхронизации всегда устанавливаются одни и те же граничные условия, следует корректность полученных граничных условий.
3. Выбор количества регистров для использования в граничных условиях
Для выбора граничных условий в точке синхронизации сначала необходимо ответить на вопрос, сколько регистров можно занять под граничные переменные.
Предположим, что нам известно точное количество регистров, которые необходимы для генерации кода для базового блока b — обозначим его RegistersNeeded(b). Общее количество регистров целевой архитектуры обозначим TotalRegisters. Тогда если выполняется условие
|bpost| + |bpre| + RegistersNeeded(b) < TotalRegisters, (1)
то множества регистров Regs(bpre), Regs(bp0St) и множество регистров, используемых внутри базового блока, можно выбрать не пересекающимися. Иными словами, сокращение числа свободных регистров из-за их использования в пред- и постусловиях не ухудшит код, сгенерированный для базового блока.
Теперь немного ослабим ограничение (1) за счет того, что возьмем множества Regs(bpre) и Regs(bp0St) пересекающимися. Для этого опишем, как эффективно из распределения Ьрге получить распределение bpost. Далее, если выполнено условие
|bpost| + RegistersNeeded(b) < TotalRegisters, (2)
то можно вставить соответствующий код в начало базового блока Ь, а затем выбрать множества регистров Regs(bp0St) и используемых при генерации кода блока b непересекающимися. Аналогично можно поступить в случае, когда выполнено условие
|bpre| + RegistersNeeded(b) < TotalRegisters (3)
Задачей переупорядочивания регистров назовем задачу генерации кода для пустого базового блока b с граничными условиями Ьрге и bpost. Алгоритм, который решает эту задачу назовем алгоритмом переупорядочивания регистров.
Утверждение Существует алгоритм переупорядочивания регистров генерирующий код, который использует только регистры из множества R Э Regs(bрге) U Regs(bp0St).
Доказательство Проведем доказательство конструктивно, то есть опишем как получить код, удовлетворяющий указанным условиям. Для этого будем использовать три операции:
• Spill(v, г) — сохранить содержимое регистра г в переменную v,
• Load(v, г) — загрузить значение переменной v в регистр г,
• Move(rl, г2) — скопировать содержимое регистра г2 в регистр rl.
Алгоритм будет выполняться в два этапа. На первом этапе все регистры освобождаются с помощью операции Spill. На втором нужные переменные загружаются в нужные регистры. Ч.т.д.
Приведенный алгоритм достаточен для доказательства утверждения, однако он генерирует избыточное количество сохранений в память и чтений из нее. Далее приведем более эффективный алгоритм генерации кода для такого базового блока, поскольку он будет использован в дальнейшем как часть комбинированного алгоритма распределения регистров.
Обозначим текущее распределение регистров bcur. Изначально оно совпадает с Ьрге. Построим ориентированный граф Gr, вершинами которого будут являться регистры целевой архитектуры. Дуга г2) присутствует в графе тогда и только тогда, когда существует переменная v, такая что (v, гх) 6 bcur, (v, r2) £
bpost
и rx Ф Г2- То есть содержимое регистра г^ необходимо переместить в регистр г2.
По определению граничных условий в графе Gr в каждую вершину входит не более одной дуги и выходит также не более одной дуги. Значит граф представляет собой совокупность цепочек и циклов. Рассмотрим, как различные операции изменяют граф.
• Операция Spill может быть применена только к регистру, в котором хранится некоторая переменная. При этом дуга, выходящая из соответствующей вершины, исчезает, если она была.
• Операция Load может быть применена только к регистру, в котором не хранится никакая переменная. При этом появится дуга, выходящая из соответствующей вершины. Случай загрузки переменной, не входящей в множество Var(bpost), рассматриваться не будет.
• Операция Move может быть применена только к паре регистров (г1( г2) таких, что в г^не хранится никакая переменная, а в г2 хранится некоторая переменная. При этом если из вершины г2 выходила дуга е = (г2, г), то она исчезнет, а вместо нее добавится дуга е' = (гх, г).
Перейдем к описанию алгоритма. Он будет состоять из нескольких шагов.
1. Все регистры, содержащие переменные, не входящие во множество Vars(bp0St), освобождаются с помощью операций Spill. После этого шага все регистры, из которых в графе GrHe выходит дуги являются свободными.
2. До тех пор, пока существует пара регистров (г1( г2), такая что в GrecTb дуга из г2в гхи нет дуги исходящей из г1; к ним применяется операция Move(r1,г2). В результате это операции исчезает дуга (г2,г-l). После завершения данного шага в графе GrHe останется цепочек.
3. До тех пор, пока в графе Gj-существует цикл, он «разрывается», а шаг 2 повторяется. «Разорвать» цикл можно двумя способами:
о переместив с помощью операции Move содержимое одного из
регистров, входящих в цикл, в свободный; о сбросив содержимое одного из регистров, входящих в цикл, в память, с помощью операции Spill.
1. Второй способ имеет смысл применять только в том случае, если свободного регистра не нашлось (то есть на регистрах из множества R хранятся |R| различных переменных). Заметим, что сбрасывать переменную на этом шаге алгоритма потребуется не более одного раза, поскольку после этого на регистрах останется |R|-1 переменная, и свободный регистр всегда найдется.
2. После завершения предыдущего шага в графе Gr не осталось ни одной дуги. Осталось загрузить на регистры недостающие переменные (то есть переменные из множества Vars(bp0St) \ Vars(bcur)). Все нужные регистры уже свободны, так как в графе Gr к этому моменту нет ни одной дуги.
Ни один из шагов алгоритма не увеличивает в графе Gr количество дуг. Каждая итерация шага 2 уменьшает количество дуг на 1. После каждой итерации шага 3 выполняется хотя бы одна итерация шага 2. Значит алгоритм конечен. Приведем псевдокод полученного в ходе доказательства леммы алгоритма. Будем считать, что граф Gr уже построен и что операции Spill, Load и Move корректно его обновляют. Операция Break-Cycle «разрывает» цикл в графе одним из приведенных в описании шага 3 способов.
procedure Reg-Reorder(bpre, bpost, Gr)
for all (v,r) 6 bpre: v 6 Vars(bpre) \ Vars(bpost)do Spill(v, r)
end for
while 3(u,v) 6 Edges(Gr): u Ф vdo for all
(r2> ri) £ Edges(Gr): 3r3: (rx r3) 6 Edges(Gr) do Move^,^)
end for
if 3c 6 Cycles(Gr)then Break-Cycle(C)
end if end while
for all (v,r) 6 bpost: v 6 Vars(bp0St) \ Vars(bpre)do Load(v, r)
end for end procedure
206
Посчитаем, сколько операций загрузки (L), сохранения (S) и пересылки (М) регистров генерирует данный алгоритм. Для этого сначала определим, в каком случае на шаге 3 алгоритма приходится прибегать к сбросу содержимого регистра в память. В момент выполнения шага 3 на регистрах могут находиться только переменные из множества Vars(bpre) П Vars(bp0St). Значит
|Vars(bpre) П Vars(bp0St)| > |R| > |Regs(bpre) U Regs(bp0St)|.
С другой стороны
|Vars(bpre) П Vars(bp0St)| < |Vars(bpre)| = |Regs(bpre)| <
< | Regs (bpre) U Regs (bpost)|
|Vars(bpre) П Vars(bp0St)| < |Vars(bp0St)| = |Regs(bp0St)| <
< |Regs(bpre) U Regs(bp0St)|
Такое возможно только если во всех нестрогих неравенствах достигается равенство. То есть
|Vars(bpre) П Vars(bp0St)| = |Vars(bpre)| = |Vars(bp0St)| => Vars(bpre) = Vars(bp0St),
|Regs(bpre) U Regs(bpost)| = |R| = |Regs(bpre)| = |Regs(bp0St)| ^ Regs (bpre) = Regs(bp0St) = R.
Возможны два случая.
• Если bpre = bpost, то алгоритм переупорядочивания на шаге 3 не будет генерировать дополнительных сбросов в память.
• Если Ьрге Ф bpost, то граф Gr будет представлять собой совокупность циклов и алгоритм переупорядочивания регистров на шаге 3 сгенерирует один дополнительный сброс в память.
Таким образом, сброс содержимого регистра в память на шаге 3 алгоритма переупорядочивания регистров происходит тогда и только тогда, когда
bpre ф bpost д Vars(bpre) = Vars(bp0St) A Regs(bpre) = Regs(bp0St) = R (4)
Вернемся к вычислению величин L, S и М.
На первом шаге алгоритма произойдет |Vars(bpre) \ Vars(bp0St)| операций Spill.
На втором шаге алгоритма произойдет | Edges (Gr) | — 1 операций Move в случае выполнения условия (4), либо |Edges (Gr)| в противном случае. На третьем шаге алгоритма произойдет |Cycles(Gr)| — 1 операций Move и одна операция Spill в случае выполнения условия (2), либо |Cycles(Gr)| операций Move в противном случае.
На четвертом шаге алгоритма произойдет |Vars(bp0St) \ Vars(bpre)| + 1 операций Load в случае выполнения условия (4), либо |Vars(bp0St) \ Vars(bpre)| в противном случае. Значит, если выполнено условие (4), то
L = |Vars(bp0St) \Vars(bpre)| + 1 = 1, S = |Vars(bpre) \ Vars(bp0St)| +1 = 1, M = I Edges (Gr) I + |Cycles(Gr)| - 2.
Иначе
L = |Vars(bp0St) \ Vars(bpre)|, S = |Vars(bpre) \ Vars(bp0St)|,. M = I Edges (Gr) I + I Cycles (Gr) I
Пусть есть два алгоритма переупорядочивания регистров. Алгоритм Ах генерирует Lx операций Load, Sx операций Spill и Мх операций Move. Алгоритм А2 генерирует L2 операций Load, S2 операций Spill и М2 операций Move. Будем считать, что алгоритм Арффективнее алгоритма А2тогда и только тогда, когда Lx + Sx < L2 + S2 либо Lx + Sx = L2 + S2 и Мх < M2.
Утверждение Описанный алгоритм переупорядочивания регистров является наиболее эффективным среди всех алгоритмов переупорядочивания регистров, использующих только регистры из множества R Э Regs(bpre) U Regs(bp0St). Доказательство (от противного)
Пусть существует алгоритм А', который эффективнее описанного. Описанный алгоритм генерирует L операций Load, S операций Spill и М операций Move. Алгоритм А' генерирует L' операций Load, S' операций Spill и М' операций Move. Отдельно рассмотрим два случая: когда условие (4) выполняется, и когда оно не выполняется.
Предположим, что условие (4) не выполняется. Поскольку переменные из множества Vars(bpre) \ Vars(bp0St) должны быть сохранены, то
S' > |Vars(bpre) \ Vars(bp0St)| = S
Аналогично
L' > |Vars(bp0St) \Vars(bpre)| = L
Поскольку L' + S' < L + S, во всех неравенствах достигается равенство. Значит в алгоритме А' никаких сохранений, кроме сохранений переменных из множества Vars(bpre) \ Vars(bp0St) нет. Данные сохранения не меняют граф Gr. Значит количество дуг и циклов в графе Gr, может уменьшаться только за счет операций Move. Так как целевой регистр операции Move должен быть свободным, каждая операция может либо уменьшать количество циклов в графе на 1, либо уменьшать количество ребер в графе на 1. Значит
М' > I Edges (Gr)| + I Cycles (Gr) I = M.
Это противоречит тому, что алгоритм А' эффективнее описанного алгоритма. Осталось рассмотреть второй случай. Пусть условие (4) выполняется. Поскольку все регистры из множества R в начале работы алгоритма заняты, первой инструкцией сгенерированного кода может быть только операция Spill, примененная к одной из переменных из множества Vars(bp0St). Значит S' > 1. Однако эта переменная в конце должна располагаться на регистре. Значит она будет загружена с помощью Load, то есть L' > 1. Так как L' + S' < L + S = 2, то L' = 1, S' = 1 и L' + S' = L + S.
Каждая операция Spill может уменьшить количество циклов графа Gr не более чем на 1. Аналогично она может уменьшить количество дуг не более чем на 1. Тогда аналогично предыдущему случаю
Это противоречит тому, что алгоритм А' эффективнее описанного алгоритма. Величина RegistersNeeded(b), используемая в условиях (2) и (3), априори неизвестна и не может быть легко вычислена. Оценим ее приближенно. Для этого введем понятие регистрового давления.
Регистровым давлением в инструкции I из базового блока Ь будем называть минимальное количество регистров, необходимых для генерации кода данной инструкции в предположении, что все переменные, которые живы в данной точке базового блока и используются в инструкции I или после нее, располагаются на регистрах.
1^Ргез5иге(1, Ь) = |Ь1уеУапаЬ1ез(1,Ь)| + |Ех1та1^151ег5(1)|
В этом определении множество живых переменных Ь1уеУапаЫез(1,Ь) может быть взято из результатов анализа времени жизни переменных. Множество дополнительных регистров Ех1гаК^181ег8(1), которые нужны инструкции I, зависит только от типа самой инструкции. Так, например, для вызова функции это множество будет состоять из регистров, которые могут быть испорчены вызываемой функцией, регистра, в котором хранится возвращаемое значение, и регистров, которые будут использованы для передачи параметров. Регистровым давлением в базовом блоке Ь назовем максимальное среди регистровых давлений во всех его инструкциях.
Регистровое давление является оценкой сверху на величину RegistersNeeded, поэтому перепишем условия (2) и (3) используя новое определение:
М' > | Edges (Gr)| - 1 + | Cycles (Gr) | - 1 = |Edges(Gr)| + |Cycles(Gr) | - 2 = M
RegPressure(b) = max(RegPressure(I, b))
ieb
|bpost| < TotalRegs - RegPressure(b), |bpre| < TotalRegs — RegPressure(b)
(5)
(6)
Теперь, если использовать для граничных условий в точке синхронизации J не более
TotalRegs — max(RegPressure(src(e)), RegPressure(dst(e))) eej
регистров, то одно из условий (5) и (6) будет обязательно выполнено во всех прилегающих к J базовых блоках.
Включение переменной в граничные условия позволяет ей пересекать границы базовых блоков на регистрах и избежать лишнего сброса этой переменной в память с последующей загрузкой ее из памяти, если она используется в нескольких базовых блоках.
Для выбора конкретных переменных для граничных условий введем функцию полезности включения переменной х в граничные условия точки синхронизации J: Usefullness(x,J). Функция полезности будет вычислять по следующей формуле:
Usefullness(x,J) = |е: е 6 J Ах 6 Vars(src(e)) Ах 6 Vars(dst(e))|.
Данная формула описывает, сколько ребер входит в точку синхронизации таких, что переменная х используется как в базовом блоке из которого данное ребро исходит, так и в базовом блоке в которое данное ребро входит. Итоговый алгоритм вычисления граничных условий в точке синхронизации J выглядит следующим образом.
function Compute-Register-Mapping(J)
р <- max(RegPressure(src(e)), RegPressure(dst(e))) eej
n <- TotelRegs — p result <- 0
priority <-Compute-Usefullness(J) for i <- l,ndo
result <- result U {(priority[i], register[i])}
end for return result end function
4. Экспериментальные результаты
Описанный алгоритм был реализован в QEMU версии 1.0. При хранении графа потока управления учитывается такая особенность QEMU, что из базового блока может исходить не более двух дуг. Вычисление точек синхронизации, сбор информации о регистровом давлении в базовом блоке, вычисление функции полезности переменной в точке синхронизации и запись граничных условий делается с помощью обхода графа GE = (Е, F) в глубину. Строить его в
явном виде не нужно. Все вершины, смежные с вершиной e=(src,dst) графа GE, можно найти просмотрев все исходящие из вершины src графа G дуги, и все входящие в dst.
Тестирование данного алгоритма начнем с модельного примера. В качестве такого примера возьмем последовательность инструкций архитектуры ARM addlt r1, r1, r2 sub r2, r2, r1 subgt r1,r1,r2 add r2, r2, M
выполненную в цикле ЬООООООСНбраз. Условные инструкции обеспечат наличие нескольких базовых блоков внутри одного блока трансляции. Гостевые регистры rl и г2 станут глобальными переменными, используемыми в нескольких базовых блоках. Выполнение данного фрагмента большое количество раз в цикле позволит провести измерение времени. Результаты тестирования приведены в таблице 1. Ускорение получается за счет того, что переменные, соответствующие гостевым регистрам rl и г2 в случае глобального распределения регистров загружаются на регистры основной машины один раз в начале обработки инструкции addlt, а в случае локального — перед обработкой каждой инструкции. Таким образом экономится по 6 загрузок и сохранений на каждом выполнении приведенного фрагмента.
Табл. 1. Результаты тестирования глобального распределения на модельном примере
Table 1. Global register allocation testing results for model example
Без глобального распределения регистров (секунд) С глобальным распределением регистров (секунд) Ускорение
16.644 11.715 29.6%
Для того, чтобы определить изменение производительности на реальных программах были использованы тесты из набора SPEC CINT2000. Тестирование на них показало небольшое падение производительности на большинстве из тестов. Результаты тестирования приведены в таблице 2. На момент написания данной статьи удалось установить две существенные причины падения производительности, которые планируется устранить в ходе дальнейших работ над данным алгоритмом.
Первая из них связана с применимостью данного алгоритма. Для того, чтобы он был применен, необходимо чтобы
• блок трансляции состоял из нескольких базовых блоков,
• некоторые глобальные переменные использовались в нескольких базовых блоках.
Табл. 2. Результаты тестирования глобального распределения на тестах из набора SPEC CINT2000
Table 2. Global register allocation testing results for SPEC CINT2000 benchmarks
Тест Без глобального распределения регистров (секунд) С глобальным распределением регистров (секунд) Ускорение
164.gzip 81.004 81.840 -1%
175.vpr 144.56 148.164 -2.5%
256.bzip2 42.496 40.580 4.5%
300.twolf 36.508 36.544 0%
Табл. 3. Результаты профилирования глобального распределения решистров
Table 3. Profiling results for global register allocation
Тест Всего блоков трансляции Блоков трансляции состоящих из нескольких базовых блоков Блоков трансляции, на которых произошло глобальное распределние регистров
164.gzip 3526 651 365
175.vpr 8016 1447 801
256.bzip2 3003 689 388
300.twolf 8221 1631 911
Как удалось выяснить с помощью профилирования, таких блоков всего около 10% от общего числа блоков трансляции. Результаты профилирования для трех тестов приведены в таблице 3. Данная проблема может быть устранена за счет увеличения блоков трансляции таким образом, чтобы они могли включать несколько гостевых базовых блоков.
Вторая причина связана с недостаточным учетом граничных условий при локальном распределении регистров. Инициализации начальными условиями и обеспечения выполнения конечных условий недостаточно. Необходимо также отдавать предпочтение регистру из граничных условий при выборе регистра для переменной, а также по возможности не занимать регистры из постусловий под переменные в них не входящие. Для устранения данной причины 212
необходимо внести дополнительные модификации в существующий алгоритм локального распределения регистров.
Список литературы
[1]. Батузов К. Задача локального распределения регистров во время динамической двоичной трансляции. Труды ИСП РАН, том 22, 2012 г., стр. 67-76. DOI: 10.15514/ISPRAS-2012-22-5
[2]. Chaitin G. J. Register allocation & spilling via graph coloring. Proceedings of the 1982 SIGPLAN symposium on Computer construction. SIGPLAN '82. New York, USA: ACM, 1982, pp. 98-105.
[3]. Poletto Massimiliano Sarkar Vivek. Linear san regsiter allocation. ACM Transaction on Programming Languages and Systems. 1999 Vol. 21, no 5, pp 895-913.
[4]. Bellard Fabrice. QEMU, a fast and portable dynamic translator. Proceedings of the annual conference on USENIX Annual Technical Conference. ATEC '05. Berkley, USA: USENIX Association, 2005, pp. 41-46.
Global register allocation during dynamic binary translation
K.A. Batuzov <[email protected]> 1 Institute for System Programming of the Russian Academy of Sciences, 25, Alexander Solzhenitsyn St., Moscow, 109004, Russia.
Abstract. Register allocation have a significant impact on performance of generated code. This paper explores the problem of global register alloction during dynamic binary translation. Since existing algorithms are designed for compilers and they are not always suitable for dynamic binary translation, a new global register allocation was developed. This algorithm decides which variables should reside on which registers before and after each basic block (called pre-and post- conditons of this basic block) and solves local register allocation problem in these constraints. To ensure that pre- and post- conditions of different basic blocks are consistent the algorithms must choose these conditions in such a way that for each basic block b'that precides arbitrary block b it's postconditions are the same as preconditions of b. This can be achieved by finding groups of arcs in control flow graph on which these conditions should remain the same (let's call them synchronisation points) and then choosing conditions for each such synchronisation point. Synchronization points are connected components in graph Ge which is a graph where arcs of original CFG are vertices and edges connect arcs which start or end in the same basic block. This gives an efficient way to find synchronization points. To choose which variables should reside on registers in each synchronization point the amount of available register is estimated using register pressure in incident basic blocks. Then actual variables a picked based on how often they are used in incident basic blocks. Finally the local register allocation algorithm is modified to use precondition and ensure post conditions of the basic block. The efficient algorithm to convert existing allocation to the desired postcondition at the end of basick block is presented with the proof of that it's optimal in terms of generated spills, reloads and register moves. The described algorithm showed 29.6% running time
decrease on the synthetic example. The needed modifications of the algorithm to efficiently run on real life application are explored.
Keywords: global register allocation, dynamic binary translation, QEMU.. DOI: 10.15514/ISPRAS-2016-28(5)-12
For citation: K.A. Batuzov. Global register allocation during dynamic binary translation. Trudy ISP RAN/Proc. ISP RAS, vol. 28, issue 5, 2016, pp. 199-214 (in Russian). DOI: 10.15514/ISPRAS-2016-28(5)-12
References
[1]. Batuzov K. Local register allocation during dynamic binary translation. Trudy ISP RAN/Proc. ISP RAS, vol. 22, 2012, pp. 67-76 (in Russian). DOI: 10.15514/ISPRAS-2012-22-5
[2]. Chaitin G. J. Register allocation & spilling via graph coloring. Proceedings of the 1982 SIGPLAN symposium on Computer construction. SIGPLAN '82. New York, USA: ACM, 1982, pp. 98-105.
[3]. Poletto Massimiliano Sarkar Vivek. Linear san regsiter allocation. ACM Transaction on Programming Languages and Systems. 1999 Vol. 21, no 5, pp 895-913.
[4]. Bellard Fabrice. QEMU, a fast and portable dynamic translator. Proceedings of the annual conference on USENIX Annual Technical Conference. ATEC '05. Berkley, USA: USENIX Association, 2005, pp. 41-46.