Зарегистрируйте распределение и разлив, простой способ?

Я ищу способ разместить локальные переменные в регистрах. Я знаю несколько серьезных методов для этого (а именно те, которые упомянуты в Википедии) , но я застрял в том, как выполняется "проливание". Кроме того, соответствующая литература довольно устрашающа. Я надеюсь, что есть что-то попроще, которое удовлетворит мои приоритеты:

  1. Корректность - алгоритм, который будет генерировать правильный код независимо от количества локальных переменных.
  2. Простота - это то, что я могу понять, не читая слишком много литературы.
  3. Эффективность - она ​​должна быть лучше, чем текущий метод, а именно:

Перевести операцию x = y # z на:

movl y, %eax
movl z, %ebx
op %ebx, %eax
movl %eax, x

Поскольку я нацелен на Intel 386, некоторые важные ограничения:

  • Бинарные операции принимают два аргумента, один из которых является источником и местом назначения. Унарные операции принимают единственный аргумент.
  • Операции могут обращаться только к одной ячейке памяти; поэтому для бинарных операций нужен хотя бы один аргумент в регистре.
  • Доступно максимум шесть регистров: %eax %ebx %ecx %edx %esi %edi. (%ebp также можно включить в крайнем случае.)
  • Есть особые случаи, например, для целочисленного деления и регистров возврата, но пока я могу их игнорировать.

В настоящий момент компилятор выполняет три этапа:

  • i386ification: все операции преобразуются в форму a = a # b (или a = #a для унарных операций).
  • Анализ живучести: определяются наборы живых переменных до и после каждой операции.
  • Размещение регистров: строится и раскрашивается интерференционный граф.

А потом компилятор подбрасывает мелки в воздух и не знает, что делать дальше.

Пример

public int mf(int cr, int ci) {
    int i = 0;
    int zr = 0;
    int zi = 0;

    while (i < 100 && zr*zr + zi*zi < 4) {
        int t = zr * zr - zi * zi + cr;
        zi = 2 * zr * zi + ci;
        zr = t;

        i = i + 1;
    }
    return i;
}

Вот довольно красивый график интерференции для функции и CFG с информацией о жизнеспособности. К сожалению, изображение CFG требует некоторой вертикальной прокрутки.

Было использовано семь цветов. Я хотел бы пролить одну из них (или набор переменных, которым назначен этот цвет). Метод выбора, который не так уж важен. Что становится сложным, так это как бороться с разлитыми переменными.

Допустим, я пролил "розовый", который представляет собой набор переменных t, $t4, $t7. Это означает, что операции, относящиеся к одной из этих переменных, будут обращаться к ней из ее позиции в кадре стека, а не через регистр. Это должно работать для этого примера.

Но что, если бы программа была:

...
a = a + b
...

и и a, и b надо было пролить? Я не могу выполнить инструкцию addl b, a с двумя адресами памяти. Мне понадобится еще один запасной регистр для временного хранения одного из операндов, а это означает выделение другого цвета. Это предлагает общий метод:

  1. Если все переменные можно раскрасить r цветами, отлично!
  2. В противном случае добавьте несколько цветов и связанных с ними переменных.
  3. Если существует операция, которая обращается к двум разлитым переменным, пролейте другой цвет и используйте резервный регистр для временного хранения всех таких операций.

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

Конкретные проблемы

Основная конкретная проблема заключается в следующем: когда переменная разливается, как это влияет на сгенерированные инструкции? Все ли инструкции, использующие эту переменную, должны обращаться к ней непосредственно в памяти (из ее позиции в стеке)? Как это будет работать, если в операции используются две разлитые переменные? (Архитектура не позволяет инструкциям обращаться к двум отдельным ячейкам памяти.)

Вторичные проблемы:

  • Как определить, куда вставлять инструкции загрузки / сохранения для правильности (и, что менее важно, эффективности)?
  • Могу ли я передать переменную только на ту часть ее жизненного цикла, когда она не используется немедленно, и удалить ее позже? Так что все инструкции действуют на незаполненные регистры. Переменная может находиться в разных регистрах в разное время.
  • Могу я быть немного эффективнее в особых случаях. Например, %eax используется для возвращаемого значения, поэтому было бы неплохо, если бы возвращаемая переменная была выделена этому регистру к тому времени, когда был обнаружен возврат. Точно так же некоторые регистры являются «сохраняемыми вызываемым пользователем», поэтому, если во время вызова функции действовало меньше переменных, их выделение в регистры, не предназначенные для сохранения вызываемого, означало бы, что я могу избежать сохранения этих регистров.
  • Поможет ли форма SSA (если вообще)? Возможность исключить общие подвыражения и оценить константы может уменьшить (?) Давление в регистре, но в противном случае это будет иметь какой-либо эффект?

Аспекты, которые меня не беспокоят (прямо сейчас):

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

Обновлять

Извините за время простоя - я размышлял над полученными ответами и пытался найти простой способ начать реализацию некоторых идей. Если честно, я откладывал ...: - \

Я нашел очень красивую презентацию (к сожалению, PPT):

http://www.cs.princeton.edu/courses/archive/spr05/cos320/notes/Register%20Allocation.ppt

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

Я постараюсь в ближайшее время поработать и, надеюсь, закрою вопрос.


person Edmund    schedule 25.12.2009    source источник
comment
Вы правы: вы будете проливать неиспользуемые в настоящее время регистры, чтобы освободить место для тех, которые будут использоваться. Тогда все операции можно будет выполнять с регистрами. Чтобы ослабить это и выполнить некоторые операции непосредственно с разлитыми регистрами, измените алгоритм распределения регистров, чтобы во время выполнения инструкций «регистр-регистр» оставался незаполненным не более одного регистра ... но в этом нет необходимости.   -  person Paul    schedule 26.12.2009
comment
@ Пол: После прочтения PPT я думаю что-то вроде: addl x, y. Если один или оба находятся в регистрах, пропустите подходящую команду; в противном случае перепишите как movl y, t; addl x, t и повторный анализ жизнеспособности и т.д. другие побочные эффекты?).   -  person Edmund    schedule 11.01.2010
comment
Какой подход вы использовали для работы с инструкциями, регистры которых фиксированы, как инструкции mul / div на x86?   -  person The Mask    schedule 18.05.2014


Ответы (2)


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

Если вам когда-либо понадобится регистр, очистите использованный регистр, освободив от него значение и записав его в стек, если он грязный и активный. В конце базового блока запишите все грязные и активные регистры.

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

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

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

Я использовал этот распределитель в компиляторе SSA, YMMV.

person Keith Randall    schedule 04.01.2010
comment
Спасибо, Кейт. Я не совсем уверен, как адаптировать вашу схему к моей работе: все мои базовые блоки минимальны, поскольку у меня есть CFG, где каждая вершина представляет собой отдельную инструкцию. Можно было бы максимизировать их (а затем линеаризовать блоки), но я привык думать о существующей модели, и все остальные этапы анализа зависят от нее. Жадный подход определенно привлекателен - я не борюсь за приз за самый эффективный компилятор :-). - person Edmund; 11.01.2010
comment
Все, что находится в памяти на основных границах блоков, может быть довольно легко преобразовано в вашу ситуацию. По сути, расположите свои базовые блоки в любом желаемом порядке, затем обработайте все свои инструкции за один проход от начала до конца, очищая кеш регистров после каждого нелокального (не из непосредственно предшествующей инструкции) на краю, и обратная запись любых грязных данных в реальном времени перед каждым нелокальным выходом. - person Keith Randall; 12.01.2010

Во-первых: не существует разумного способа сделать это. Проблема NP-полная ;-)

Как происходит разлив:

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

Как работать с EAX:

Отметьте регистр как заполненный, но не храните в нем никаких переменных (предварительное выделение). Это заставит генератор кода очистить этот регистр. Чтобы быть умным, сохраните значение в другом регистре, если это выгодно.

Простые и правильные способы справиться с разливом:

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

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

Конкретные ответы

  1. Что для вас значит правильность? Даже простые алгоритмы распределения будут правильными, если вы не сделаете ошибки программирования. Доказательство (математической) правильности намного сложнее. Перед тем, как значение / регистр снова понадобится, необходимо вставить как загрузки, так и сохранения. Оба должны быть вставлены после сохранения / создания значения.

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

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

  4. При определенных условиях SSA действительно помогает. Графы вывода кода SSA всегда хордовые, что означает, что нет цикла с более чем 3 узлами. Это частный случай раскраски графа, в котором минимальную раскраску можно найти за полиномиальное время. Переход на SSA не обязательно означает большее или меньшее давление регистрации. Хотя в форме SSA обычно больше переменных, у них, как правило, меньше время жизни.

person ebo    schedule 25.12.2009
comment
Спасибо, ebo, это хорошо написанная статья, и подход линейного сканирования может быть осуществимым (т.е. достаточно простым для меня, чтобы понять!). Вы действительно не обратились к тем частям, о которых я думаю; это потому, что я не понимал их. Я добавлю подробностей к вопросу. Выделить пространство стека достаточно просто (и его можно оптимизировать для объединения не мешающих переменных). То же самое и с eax: я просто не могу считать его доступным регистром и использовать его только для возвращаемых значений (но это пустая трата хорошего регистра). - person Edmund; 26.12.2009
comment
Ссылка на статью кажется мертвой. Есть ли шанс снова найти его и опубликовать ссылку? - person Leonard Schütz; 08.02.2018
comment
Это М. Полетто и В. Саркар, Размещение регистров линейного сканирования. Похоже, что citeseer какое-то время не работал, так как я снова могу перейти по ссылке. - person ebo; 08.02.2018