Che cosa fa esattamente l'istruzione PHI e come usarla in LLVM


91

LLVM ha istruzioni phi con una spiegazione piuttosto strana:

L'istruzione "phi" viene utilizzata per implementare il nodo φ nel grafico SSA che rappresenta la funzione.

In genere viene utilizzato per implementare la ramificazione. Se ho capito bene, è necessario per rendere possibile l'analisi delle dipendenze e in alcuni casi potrebbe aiutare a evitare caricamenti non necessari. Tuttavia è ancora difficile capire cosa fa esattamente.

L' esempio del caleidoscopio lo spiega abbastanza bene per il ifcaso. Tuttavia non è così chiaro come implementare operazioni logiche come &&e ||. Se digito quanto segue nel compilatore llvm online :

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

Le ultime righe mi confondono completamente:

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

Sembra che il nodo phi produca un risultato che può essere utilizzato. E avevo l'impressione che il nodo phi definisca semplicemente da quali percorsi provengono i valori.

Qualcuno potrebbe spiegare cos'è un nodo Phi e come implementarlo ||?


1
Il phinodo è una soluzione del problema nei compilatori per convertire l'IR nella forma "Statica singola assegnazione". Per capire meglio capire la soluzione suggerirei di capire meglio il problema. Quindi ti chiameròphi " Why is node ".
Vraj Pandya

Risposte:


77

Un nodo phi è un'istruzione utilizzata per selezionare un valore a seconda del predecessore del blocco corrente (guarda qui per vedere la gerarchia completa - è anche usato come valore, che è una delle classi da cui eredita).

I nodi Phi sono necessari a causa della struttura dello stile SSA (assegnazione singola statica) del codice LLVM, ad esempio la seguente funzione C ++

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

viene tradotto nel seguente IR: (creato attraverso clang -c -emit-llvm file.c -o out.bc- e poi visualizzato attraverso 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
}

Allora cosa succede qui? A differenza del codice C ++, dove la variabile bool lpuò essere 0 o 1, nell'IR LLVM deve essere definita una volta . Quindi controlliamo se %toboolè vero e poi passiamo a lor.endo lor.rhs.

In lor.endabbiamo finalmente il valore di || operatore. Se siamo arrivati ​​dal blocco di ingresso, allora è proprio vero. Altrimenti, è uguale al valore di %tobool2- ed è esattamente ciò che otteniamo dalla seguente riga IR:

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

6
TL; DR φ node è un'espressione ternaria. Si potrebbe obiettare che non contiene la condizione, ma in realtà, dopo la conversione nel codice finale, non è possibile determinare altrimenti quale degli argomenti è attivo, quindi anche φ deve avere la condizione.
Hi-Angel,

31

Non è affatto necessario utilizzare phi. Basta creare un mucchio di variabili temporanee. I passaggi di ottimizzazione LLVM si occuperanno di ottimizzare le variabili temporanee e utilizzeranno automaticamente il nodo phi per questo.

Ad esempio, se vuoi fare questo:

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

Puoi usare il nodo phi per quello (in pseudocodice):

  1. assegna 4 a x1
  2. se (! qualcosa) si dirama a 4
  3. calcola x2 da x1 aggiungendo 2
  4. assegna x3 phi da x1 e x2
  5. chiama print con x3

Ma puoi fare a meno del nodo phi (in pseudocodice):

  1. alloca la variabile locale sullo stack chiamato x
  2. caricare nel valore temp x1 4
  3. memorizzare da x1 a x
  4. se (! qualcosa) si dirama a 8
  5. carica x alla temp x2
  6. aggiungi x2 con 4 a temp x3
  7. memorizzare da x3 a x
  8. carica x alla temperatura x4
  9. chiama print con x4

Eseguendo passaggi di ottimizzazione con llvm, questo secondo codice verrà ottimizzato per il primo codice.


4
Da quello che ho letto sembra che ci siano alcune restrizioni da tenere a mente qui. mem2reg è il passaggio di ottimizzazione in questione e presenta alcune limitazioni evidenziate nell'esempio Kaleidoscope . Sembra che questo sia, tuttavia, il modo preferito per gestire il problema e viene utilizzato da Clang.
Matthew Sanders
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.