Что именно делает инструкция PHI и как ее использовать в LLVM

87

В LLVM есть инструкция phi с довольно странным объяснением:

Команда phi используется для реализации узла φ в графе SSA, представляющем функцию.

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

Пример с калейдоскопом довольно хорошо объясняет это для ifслучая. Однако не совсем понятно, как реализовать логические операции вроде &&и ||. Если я введу в онлайн- компилятор llvm следующее :

void main1(bool r, bool y) {
    bool l = y || r;
}

Последние несколько строк меня полностью сбивают с толку:

; <label>:10                                      ; preds = %7, %0
%11 = phi i1 [ true, %0 ], [ %9, %7 ]
%12 = zext i1 %11 to i8

Похоже, что phi-узел дает результат, который можно использовать. И у меня создалось впечатление, что узел phi просто определяет, из каких путей приходят значения.

Может ли кто-нибудь объяснить, что такое узел Phi и как его реализовать ||?

vwvw
источник
1
phiУзел представляет собой решение задачи в компиляторах для преобразования ИК в «Статический сингл назначения» форме. Чтобы лучше понять решение, я предлагаю лучше понять проблему. Итак, я расскажу вам " Почему phiузел ".
Vraj Pandya

Ответы:

76

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

Узлы Phi необходимы из-за структуры стиля SSA (static single assignment) кода LLVM - например, следующая функция C ++

void m(bool r, bool y){
    bool l = y || r ;
}

переводится в следующий IR: (создается через clang -c -emit-llvm file.c -o out.bc- и затем просматривается llvm-dis)

define void @_Z1mbb(i1 zeroext %r, i1 zeroext %y) nounwind {
entry:
  %r.addr = alloca i8, align 1
  %y.addr = alloca i8, align 1
  %l = alloca i8, align 1
  %frombool = zext i1 %r to i8
  store i8 %frombool, i8* %r.addr, align 1
  %frombool1 = zext i1 %y to i8
  store i8 %frombool1, i8* %y.addr, align 1
  %0 = load i8* %y.addr, align 1
  %tobool = trunc i8 %0 to i1
  br i1 %tobool, label %lor.end, label %lor.rhs

lor.rhs:                                          ; preds = %entry
  %1 = load i8* %r.addr, align 1
  %tobool2 = trunc i8 %1 to i1
  br label %lor.end

lor.end:                                          ; preds = %lor.rhs, %entry
  %2 = phi i1 [ true, %entry ], [ %tobool2, %lor.rhs ]
  %frombool3 = zext i1 %2 to i8
  store i8 %frombool3, i8* %l, align 1
  ret void
}

Так что здесь происходит? В отличие от кода C ++, где переменная bool lможет иметь значение 0 или 1, в LLVM IR ее нужно определять один раз . Итак, мы проверяем, %toboolправда ли, а затем переходим к lor.endили lor.rhs.

В lor.endконце концов мы имеем значение из || оператор. Если приехали из въездного блока - значит, это правда. В противном случае оно равно значению %tobool2- и это именно то, что мы получаем из следующей ИК-линии:

%2 = phi i1 [ true, %entry ], [ %tobool2, %lor.rhs ]
Гай Адини
источник
6
TL; DR φ узел - троичное выражение. Кто-то может возразить, что он не содержит условия, но на самом деле после преобразования в окончательный код вы не можете иначе определить, какой из аргументов является живым, поэтому φ тоже должно иметь условие.
Hi-Angel
31

Вам вообще не нужно использовать фи. Просто создайте кучу временных переменных. Проходы оптимизации LLVM позаботятся об оптимизации временных переменных и будут использовать для этого phi node автоматически.

Например, если вы хотите сделать это:

x = 4;
if (something) x = x + 2;
print(x);

Для этого вы можете использовать phi node (в псевдокоде):

  1. назначить 4 на x1
  2. если (! something) перейти на 4
  3. вычислить x2 из x1, добавив 2
  4. назначить x3 phi от x1 и x2
  5. вызов печати с x3

Но можно обойтись без phi node (в псевдокоде):

  1. выделить локальную переменную в стеке с именем x
  2. загрузить в temp x1 значение 4
  3. хранить x1 в x
  4. если (! что-то) перейти на 8
  5. загрузить x до температуры x2
  6. добавить x2 с 4 к температуре x3
  7. хранить x3 в x
  8. загрузить x до температуры x4
  9. вызов печати с x4

При выполнении проходов оптимизации с llvm этот второй код будет оптимизирован для первого кода.

Мартиньш Можейко
источник
4
Из того, что я прочитал, похоже, что здесь есть несколько ограничений. mem2reg - это этап оптимизации, о котором идет речь, и у него есть несколько ограничений, которые указаны в примере Kaleidoscope . Однако похоже, что это предпочтительный способ решения проблемы, и он используется Clang.
Мэтью Сандерс