Регистрация распределения и разлива, легкий способ? - PullRequest
26 голосов
/ 25 декабря 2009

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

  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. Если существует операция, которая обращается к двум разлитым переменным, выведите другой цвет и используйте резервный регистр для временного хранения всех таких операций.

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

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

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

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

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

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

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

Обновление

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

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

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

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

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

Ответы [ 2 ]

10 голосов
/ 05 января 2010

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

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

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

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

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

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

7 голосов
/ 26 декабря 2009

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

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

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

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

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

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

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

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

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

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

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

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

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

...