Подтвердить что ты не робот

Компилятор написан на Java: реализация оптимизатора Peephole

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

Спецификация оптимизатора Peephole

Я исследовал несколько разных подходов к написанию оптимизатора подглядывания, и я решил использовать обратный подход:

  • Encoder выполняет вызов функции emit() каждый раз, когда должна быть сгенерирована машинная инструкция.
  • emit(Instruction currentInstr) проверяет таблицу оптимизации глазок:
    • Если текущая команда соответствует хвосту шаблона:
      • Проверить ранее выпущенные инструкции для соответствия
      • Если все команды совпадают с шаблоном, примените оптимизацию, изменив конец конца хранилища кодов
    • Если оптимизация не найдена, введите команду как обычно

Текущий подход к разработке

Этот метод достаточно прост, это реализация, с которой у меня возникают проблемы. В моем компиляторе машинные инструкции хранятся в классе Instruction. Я написал класс InstructionMatch, где хранятся регулярные выражения, предназначенные для соответствия каждому компоненту машинной команды. Его метод equals(Instruction instr) возвращает true, если шаблоны соответствуют некоторой машинной команде instr.

Однако я не могу полностью применить правила, которые у меня есть. Во-первых, я чувствую, что, учитывая мой нынешний подход, у меня будет беспорядок ненужных объектов. Учитывая, что полный список номеров оптимизации глазок может насчитывать около 400 шаблонов, это быстро выходит из-под контроля. Кроме того, я не могу получить более сложные замены, работающие с этим подходом (см. "Мой вопрос" ).

Альтернативные подходы

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

Примеры шаблонов, синтаксис шаблонов

x: JUMP x+1; x+1: JUMP y  -->  x: JUMP y
LOADL x; LOADL y; add     -->  LOADL x+y
LOADA d[r]; STOREI (n)    -->  STORE (n) d[r]

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

op_code register n d

(n обычно указывает количество слов и d смещение адреса). Синтаксис x: <instr> указывает, что инструкция хранится по адресу x в хранилище кода.

Итак, инструкция LOADL 17 эквивалентна полной машинной команде 5 0 0 17, когда код операции LOADL равен 5 (n и r не используются в этой инструкции)

Мой вопрос

Итак, учитывая этот фон, мой вопрос таков: как эффективно сопоставлять и заменять шаблоны, когда мне нужно включать части предыдущих инструкций в качестве переменных в моей замене? Например, я могу просто заменить все экземпляры LOADL 1; add инструкцией машинного приращения - для этого мне не нужна какая-либо часть предыдущих инструкций. Но я не понимаю, как эффективно использовать значения "x" и "y" моего второго примера в шаблоне замещения.

edit. Я должен упомянуть, что каждое поле класса Instruction представляет собой просто целое число (как обычно для машинных команд). Любое использование "x" или "y" в таблице шаблонов - это переменная, стоящая за любым целым значением.

4b9b3361

Ответ 1

Легкий способ сделать это - реализовать оптимизатор подглядывания как конечный конечный автомат.

Мы предполагаем, что у вас есть генератор исходного кода, который генерирует команды, но не испускает их, и процедуру генерации, которая отправляет фактический код в поток объекта.

Конечный автомат записывает инструкции, которые генерирует генератор кода, и запоминает последовательности из 0 или более сгенерированных инструкций путем перехода между состояниями. Таким образом, состояние неявно запоминает (короткую) последовательность генерируемых, но не испускаемых команд; он также должен помнить ключевые параметры команд, которые он захватил, например, имя регистра, постоянное значение и/или режимы адресации и абстрактные целевые ячейки памяти. Специальное начальное состояние запоминает пустую строку инструкций. В любой момент вы должны иметь возможность испускать неиспользуемые инструкции ( "флеш" ); если вы делаете это все время, ваш генератор глазок захватывает следующую инструкцию, а затем испускает ее, не делая никакой полезной работы.

Чтобы сделать полезную работу, мы хотим, чтобы машина фиксировала как можно более продолжительную последовательность. Поскольку обычно существует множество видов машинных инструкций, в качестве практического вопроса вы не можете запомнить слишком много подряд, или конечный автомат станет огромным. Но практично помнить последние два или три для наиболее общих машинных инструкций (загрузка, добавление, cmp, ветвь, хранилище). Размер машины действительно будет определяться длиной самой длинной оптимизации глазок, которую мы хотим сделать, но если эта длина равна P, вся машина не должна быть P-состояниями глубоко.

Каждое состояние имеет переход к следующему состоянию на основе "следующей" инструкции, которую я произвел генератором кода. Представьте, что состояние представляет собой захват N инструкций. Варианты перехода:

  • сбросить самые левые 0 или более (назовите это k) инструкции, которые это состояние представляет, и перейти к следующему состоянию, представляя N-k + 1, инструкции, которые представляют дополнительный захват машинной команды I.
  • сбросить самые левые k команд, которые представляет это состояние, переход в состояние который представляет остальные инструкции N-k и инструкцию по обработке I.
  • полностью очистить состояние и испустить команду I. Вы действительно можете сделайте это только в начальном состоянии].

При очистке k-инструкций то, что на самом деле испускается, является оптимизированной версией тех k. Вы можете вычислить все, что хотите, при выдаче таких инструкций. Вам также необходимо помнить о необходимости "сдвигать" параметры для остальных инструкций.

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

Предположим, что наша машина является дополненной машиной стека, имеет

 PUSHVAR x
 PUSHK i
 ADD
 POPVAR x
 MOVE x,k

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

О нас, о которых мы заботимся, относятся:

 PUSHK i, PUSHK j, ADD ==> PUSHK i+j
 PUSHK i, POPVAR x ==> MOVE x,i 

Наши переменные состояния:

 PEEPHOLESTATE (an enum symbol, initialized to EMPTY)
 FIRSTCONSTANT (an int)
 SECONDCONSTANT (an int)

Наши заявления о случаях:

GeneratePUSHK:
    switch (PEEPHOLESTATE) {
        EMPTY: PEEPHOLESTATE=PUSHK;
               FIRSTCONSTANT=K;
               break;
        PUSHK: PEEPHOLESTATE=PUSHKPUSHK;
               SECONDCONSTANT=K;
               break;
        PUSHKPUSHK:
        #IF consumeEmitLoadK // flush state, transition and consume generated instruction
               emit(PUSHK,FIRSTCONSTANT);
               FIRSTCONSTANT=SECONDCONSTANT;
               SECONDCONSTANT=K;
               PEEPHOLESTATE=PUSHKPUSHK;
               break;
        #ELSE // flush state, transition, and reprocess generated instruction
               emit(PUSHK,FIRSTCONSTANT);
               FIRSTCONSTANT=SECONDCONSTANT;
               PEEPHOLESTATE=PUSHK;
               goto GeneratePUSHK;  // Java can't do this, but other langauges can.
        #ENDIF
     }

  GenerateADD:
    switch (PEEPHOLESTATE) {
        EMPTY: emit(ADD);
               break;
        PUSHK: emit(PUSHK,FIRSTCONSTANT);
               emit(ADD);
               PEEPHOLESTATE=EMPTY;
               break;
        PUSHKPUSHK:
               PEEPHOLESTATE=PUSHK;
               FIRSTCONSTANT+=SECONDCONSTANT;
               break:
     }  

  GeneratePOPX:
    switch (PEEPHOLESTATE) {
        EMPTY: emit(POP,X);
               break;
        PUSHK: emit(MOV,X,FIRSTCONSTANT);
               PEEPHOLESTATE=EMPTY;
               break;
        PUSHKPUSHK:
               emit(MOV,X,SECONDCONSTANT);
               PEEPHOLESTATE=PUSHK;
               break:
     }

GeneratePUSHVARX:
    switch (PEEPHOLESTATE) {
        EMPTY: emit(PUSHVAR,X);
               break;
        PUSHK: emit(PUSHK,FIRSTCONSTANT);
               PEEPHOLESTATE=EMPTY;
               goto GeneratePUSHVARX;
        PUSHKPUSHK:
               PEEPHOLESTATE=PUSHK;
               emit(PUSHK,FIRSTCONSTANT);
               FIRSTCONSTANT=SECONDCONSTANT;
               goto GeneratePUSHVARX;
     }

#IF показывает два разных стиля переходов, один из которых потребляет сгенерированный инструкция, а другая - нет; либо работает для этого примера. Когда вы закончите с несколькими сотнями из этих случаев, вы обнаружите, что оба типа удобны, а версия "не потреблять" помогает вы сохраняете свой код меньше.

Нам нужна рутина, чтобы очистить оптимизатор глазок:

  flush() {
    switch (PEEPHOLESTATE) {
        EMPTY: break;
        PUSHK: emit(PUSHK,FIRSTCONSTANT);
               break;
        PUSHKPUSHK:
               emit(PUSHK,FIRSTCONSTANT),
               emit(PUSHK,SECONDCONSTANT),
               break:
      }
      PEEPHOLESTATE=EMPTY;
      return; }

Интересно рассмотреть, что делает этот оптимизатор головок со следующим сгенерированным кодом:

      PUSHK  1
      PUSHK  2
      ADD
      PUSHK  5
      POPVAR X
      POPVAR Y

Что делает вся эта схема FSA, это скрыть соответствие шаблонов в переходах состояний и ответ на сопоставленные шаблоны в случаях. Вы можете закодировать это вручную, и это быстро и относительно легко кодировать и отлаживать. Но когда число случаев становится большим, вы не хотите создавать такой конечный автомат вручную. Вы можете написать инструмент для создания этого конечного автомата; Хорошим фоном для этого будет генерация машинного генератора FLEX или LALR. Я не собираюсь объяснять это здесь: -}