Що саме робить інструкція PHI та як використовувати її у LLVM


87

LLVM має Фі інструкцію з досить дивним поясненням:

Інструкція '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 і як його реалізувати ||?


1
phiВузол є рішення задачі в компіляторах для перетворення ІК в «Статичний сингл призначення» формі. Щоб краще зрозуміти рішення, я б запропонував краще зрозуміти проблему. Тож я підкажу вам " Чому це phiвузол ".
Vraj Pandya

Відповіді:


76

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

Вузли Phi необхідні завдяки структурі стилю SSA (статичне єдине призначення) коду LLVM - наприклад, наступна функція C ++

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

перекладається в такий ІЧ: (створений через 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, в IR LLVM вона повинна бути визначена один раз . Тож ми перевіряємо, чи %toboolце правда, а потім переходимо до lor.endабо lor.rhs.

У lor.endнас нарешті є значення || оператора. Якщо ми прибули з блоку входу - то це просто правда. В іншому випадку воно дорівнює значенню %tobool2- і саме це ми отримуємо з наступного ІЧ-рядка:

%2 = phi i1 [ true, %entry ], [ %tobool2, %lor.rhs ]

6
TL; DR φ вузол - це трійковий вираз. Можна стверджувати, що він не містить умови, але насправді, перетворюючи остаточний код, ви не можете визначити інакше, який з аргументів є активним, тому φ також повинен мати умову.
Hi-Angel

31

Вам зовсім не потрібно використовувати фі. Просто створіть купу тимчасових змінних. Пропуски оптимізації LLVM подбають про оптимізацію тимчасових змінних і автоматично використовуватимуть для цього вузол phi.

Наприклад, якщо ви хочете зробити це:

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

Ви можете використовувати для цього вузол phi (у псевдокоді):

  1. присвоїти 4 x1
  2. якщо (! щось) гілка до 4
  3. обчислити x2 з x1, додавши 2
  4. присвоїти x3 phi з x1 і x2
  5. виклик друку з x3

Але можна обійтися і без вузла phi (у псевдокоді):

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

Запустивши оптимізаційні проходи з llvm, цей другий код буде оптимізований до першого коду.


4
З того, що я прочитав, звучить, що тут є кілька обмежень, про які слід пам’ятати. mem2reg - це прохід про оптимізацію, і він має кілька обмежень, на які вказує приклад з калейдоскопом . Здається, це, однак, найкращий спосіб вирішення проблеми, який використовується Клангом.
Метью Сандерс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.