web-dev-qa-db-fra.com

Pourquoi les compilateurs dupliquent-ils certaines instructions?

Parfois, les compilateurs génèrent du code avec des duplications d'instructions étranges qui peuvent être supprimées en toute sécurité. Considérez le morceau de code suivant:

int gcd(unsigned x, unsigned y) {
  return x == 0 ? y : gcd(y % x, x);
}

Voici le code d'assemblage (généré par clang 5. avec optimisations activées):

gcd(unsigned int, unsigned int): # @gcd(unsigned int, unsigned int)
  mov eax, esi
  mov edx, edi
  test edx, edx
  je .LBB0_1
.LBB0_2: # =>This Inner Loop Header: Depth=1
  mov ecx, edx
  xor edx, edx
  div ecx
  test edx, edx
  mov eax, ecx
  jne .LBB0_2
  mov eax, ecx
  ret
.LBB0_1:
  ret

Dans l'extrait de code suivant:

  mov eax, ecx
  jne .LBB0_2
  mov eax, ecx

Si le saut ne se produit pas, eax est réaffecté sans raison évidente.

L'autre exemple est deux ret à la fin de la fonction: l'un fonctionnerait parfaitement aussi.

Le compilateur n'est-il tout simplement pas assez intelligent ou il y a une raison pour ne pas supprimer les doublons?

58
Ignat Loskutov

Les compilateurs peuvent effectuer des optimisations qui ne sont pas évidentes pour les gens et la suppression des instructions ne rend pas toujours les choses plus rapides.

Une petite quantité de recherche montre que divers processeurs AMD ont des problèmes de prédiction de branche lorsqu'un RET est immédiatement après une branche conditionnelle. En remplissant cet emplacement avec ce qui est essentiellement un no-op, le problème de performances est évité.

Mise à jour:

Exemple de référence, la section 6.2 du "Guide d'optimisation des logiciels pour les processeurs AMD64" (voir http://support.AMD.com/TechDocs/25112.PDF ) dit:

Plus précisément, évitez les deux situations suivantes:

  • Tout type de branche (conditionnelle ou inconditionnelle) qui a pour cible l'instruction RET à retour unique sur un octet. Voir "Exemples".

  • Une branche conditionnelle qui se produit dans le code juste avant l'instruction RET de retour proche à un octet.

Il explique également en détail pourquoi les cibles de saut doivent être alignées, ce qui est également susceptible d'expliquer les RET en double à la fin de la fonction.

40
janm

Tout compilateur aura un tas de transformations pour renommer les registres, dérouler, hisser, etc. La combinaison de leurs sorties peut conduire à des cas sous-optimaux tels que ce que vous avez montré. Marc Glisse offre de bons conseils: cela vaut la peine de rapporter un bug. Vous décrivez la possibilité pour un optimiseur de judas de supprimer les instructions qui

  • n'affecte pas du tout l'état des registres et de la mémoire, ou
  • n'affecte pas l'état qui compte pour les post-conditions d'une fonction, n'a pas d'importance pour son API publique.

Cela ressemble à une opportunité pour exécution symbolique techniques. Si le solveur de contraintes ne trouve aucun point de branchement pour un MOV donné, c'est peut-être vraiment un NOP.

5
J_H