LLVM a phi instruction avec une explication assez étrange:
L'instruction 'phi' est utilisée pour implémenter le nœud dans le graphe SSA représentant la fonction.
Généralement, il est utilisé pour implémenter la création de branches. Si j'ai bien compris, il est nécessaire de rendre l'analyse de dépendance possible et, dans certains cas, d'éviter les charges inutiles. Cependant, il est encore difficile de comprendre ce qu’il fait exactement.
Kaléidoscope exemple l'explique assez bien pour if
cas. Cependant, la façon de mettre en œuvre des opérations logiques telles que &&
et ||
n'est pas claire. Si je tape ce qui suit à llvm en ligne compilateur:
void main1(bool r, bool y) {
bool l = y || r;
}
Les dernières lignes me confondent complètement:
; <label>:10 ; preds = %7, %0
%11 = phi i1 [ true, %0 ], [ %9, %7 ]
%12 = zext i1 %11 to i8
On dirait que le noeud phi produit un résultat utilisable. Et j’ai eu l’impression que le nœud phi ne fait que définir les chemins à partir desquels les valeurs proviennent.
Quelqu'un pourrait-il expliquer ce qu'est un nœud Phi et comment implémenter ||
avec celui-ci?
Un nœud phi est une instruction utilisée pour sélectionner une valeur en fonction du prédécesseur du bloc actuel (regardez ici pour voir la hiérarchie complète - elle sert également de valeur, qui est l'une des classes dont il hérite) .
Les nœuds Phi sont nécessaires en raison de la structure du style SSA (assignation unique statique) du code LLVM - par exemple, la fonction C++ suivante
void m(bool r, bool y){
bool l = y || r ;
}
est traduit dans l'IR suivant: (créé via clang -c -emit-llvm file.c -o out.bc
- puis visualisé via 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
}
Alors, que se passe-t-il ici? Contrairement au code C++, où la variable bool l
peut être 0 ou 1, dans le LLVM IR, elle doit être définie once. Nous vérifions donc si %tobool
est vrai, puis passons à lor.end
ou lor.rhs
.
Dans lor.end
nous avons enfin la valeur de || opérateur. Si nous arrivons du bloc d’entrée, c’est tout simplement vrai. Sinon, il est égal à la valeur de %tobool2
- et c'est exactement ce que nous obtenons de la ligne IR suivante:
%2 = phi i1 [ true, %entry ], [ %tobool2, %lor.rhs ]
Vous n'avez pas du tout besoin d'utiliser phi. Créez simplement un groupe de variables temporaires. Les passes d’optimisation LLVM s’occuperont de l’optimisation des variables temporaires et utiliseront automatiquement le noeud phi.
Par exemple, si vous voulez faire ceci:
x = 4;
if (something) x = x + 2;
print(x);
Vous pouvez utiliser le noeud phi pour cela (en pseudocode):
Mais vous pouvez vous passer du noeud phi (en pseudocode):
En exécutant des passes d'optimisation avec llvm, ce second code sera optimisé pour le premier code.