Pourquoi Clang optimise-t-il x * 1.0 mais PAS x + 0.0?

125

Pourquoi Clang optimise-t-il la boucle dans ce code

#include <time.h>
#include <stdio.h>

static size_t const N = 1 << 27;
static double arr[N] = { /* initialize to zero */ };

int main()
{
    clock_t const start = clock();
    for (int i = 0; i < N; ++i) { arr[i] *= 1.0; }
    printf("%u ms\n", (unsigned)(clock() - start) * 1000 / CLOCKS_PER_SEC);
}

mais pas la boucle dans ce code?

#include <time.h>
#include <stdio.h>

static size_t const N = 1 << 27;
static double arr[N] = { /* initialize to zero */ };

int main()
{
    clock_t const start = clock();
    for (int i = 0; i < N; ++i) { arr[i] += 0.0; }
    printf("%u ms\n", (unsigned)(clock() - start) * 1000 / CLOCKS_PER_SEC);
}

(Balisage à la fois en C et C ++ car j'aimerais savoir si la réponse est différente pour chacun.)

user541686
la source
2
Quels indicateurs d'optimisation sont actuellement actifs?
Je n'existerai pas Idonotexist
1
@IwillnotexistIdonotexist: Je viens d'utiliser -O3, je ne sais pas comment vérifier ce que cela active.
user541686
2
Il serait intéressant de voir ce qui se passe si vous ajoutez -ffast-math à la ligne de commande.
plugwash
static double arr[N]n'est pas autorisé en C; constles variables ne comptent pas comme des expressions constantes dans cette langue
MM
1
[Insérez un commentaire sournois sur le fait que C n'est pas C ++, même si vous l'avez déjà appelé.]
user253751

Réponses:

164

La norme IEEE 754-2008 pour l'arithmétique à virgule flottante et la norme ISO / CEI 10967 pour l'arithmétique indépendante du langage (LIA), partie 1 expliquent pourquoi il en est ainsi.

IEEE 754 § 6.3 Le bit de signe

Lorsqu'une entrée ou un résultat est NaN, cette norme n'interprète pas le signe d'un NaN. Notez, cependant, que les opérations sur les chaînes de bits - copie, négation, abs, copySign - spécifient le bit de signe d'un résultat NaN, parfois basé sur le bit de signe d'un opérande NaN. Le prédicat logique totalOrder est également affecté par le bit de signe d'un opérande NaN. Pour toutes les autres opérations, cette norme ne spécifie pas le bit de signe d'un résultat NaN, même lorsqu'il n'y a qu'une seule entrée NaN, ou lorsque le NaN est produit à partir d'une opération non valide.

Lorsque ni les entrées ni le résultat ne sont NaN, le signe d'un produit ou d'un quotient est le OU exclusif des signes des opérandes; le signe d'une somme, ou d'une différence x - y considérée comme une somme x + (−y), diffère d'au plus un des signes d'addition; et le signe du résultat des conversions, l'opération de quantification, les opérations roundTo-Integral et roundToIntegralExact (voir 5.3.1) est le signe du premier ou du seul opérande. Ces règles s'appliquent même lorsque les opérandes ou les résultats sont nuls ou infinis.

Lorsque la somme de deux opérandes avec des signes opposés (ou la différence de deux opérandes avec des signes similaires) est exactement zéro, le signe de cette somme (ou différence) doit être +0 dans tous les attributs de sens d'arrondi sauf roundTowardNegative; sous cet attribut, le signe d'une somme nulle exacte (ou d'une différence) sera de −0. Cependant, x + x = x - (−x) conserve le même signe que x même lorsque x est égal à zéro.

Le cas de l'addition

Sous le mode d'arrondi par défaut (Round-to-Nearest, Ties-to-Even) , nous voyons que x+0.0produit x, SAUF quand xest -0.0: Dans ce cas, nous avons une somme de deux opérandes avec des signes opposés dont la somme est zéro, et §6.3 paragraphe 3 règles que cet ajout produit +0.0.

Comme il +0.0n'est pas identique au niveau du bit à l'original -0.0, et qu'il -0.0s'agit d'une valeur légitime qui peut apparaître en entrée, le compilateur est obligé de mettre le code qui transformera les zéros négatifs potentiels en +0.0.

Le résumé: Dans le mode d'arrondi par défaut, dans x+0.0, six

  • n'est pas -0.0 , alors xelle-même est une valeur de sortie acceptable.
  • is -0.0 , alors la valeur de sortie doit être +0.0 , qui n'est pas identique au niveau du bit à -0.0.

Le cas de la multiplication

Dans le mode d'arrondi par défaut , aucun problème de ce type ne se produit avec x*1.0. Si x:

  • est un nombre (sous) normal, x*1.0 == xtoujours.
  • est +/- infinity, alors le résultat est +/- infinitydu même signe.
  • est NaN, alors selon

    IEEE 754 § 6.2.3 Propagation NaN

    Une opération qui propage un opérande NaN vers son résultat et qui a un seul NaN comme entrée doit produire un NaN avec la charge utile de l'entrée NaN si représentable dans le format de destination.

    ce qui signifie que l'exposant et la mantisse (mais pas le signe) de NaN*1.0sont recommandés inchangés par rapport à l'entrée NaN. Le signe n'est pas spécifié conformément au §6.3p1 ci-dessus, mais une implémentation peut spécifier qu'il est identique à la source NaN.

  • est +/- 0.0, alors le résultat est a 0avec son bit de signe XORed avec le bit de signe de 1.0, en accord avec le §6.3p2. Puisque le bit de signe 1.0est 0, la valeur de sortie est inchangée par rapport à l'entrée. Ainsi, x*1.0 == xmême quand xest un zéro (négatif).

Le cas de la soustraction

Dans le mode d'arrondi par défaut , la soustraction x-0.0est également un no-op, car elle équivaut à x + (-0.0). Si xc'est

  • is NaN, alors §6.3p1 et §6.2.3 s'appliquent à peu près de la même manière que pour l'addition et la multiplication.
  • est +/- infinity, alors le résultat est +/- infinitydu même signe.
  • est un nombre (sous) normal, x-0.0 == xtoujours.
  • c'est -0.0donc au §6.3p2 que nous avons « [...] le signe d'une somme, ou d'une différence x - y considérée comme une somme x + (−y), diffère d'au plus un des signes d'addition; ". Cela nous oblige à attribuer -0.0à la suite de (-0.0) + (-0.0), car le -0.0signe ne diffère d' aucun des compléments, tandis que le +0.0signe diffère de deux des compléments, en violation de cette clause.
  • est +0.0, alors cela se réduit au cas d'addition (+0.0) + (-0.0)considéré ci-dessus dans Le cas de l'addition , qui par §6.3p3 est censé donner +0.0.

Puisque dans tous les cas, la valeur d'entrée est légale comme sortie, il est permis de considérer x-0.0un no-op et x == x-0.0une tautologie.

Optimisations qui changent de valeur

La norme IEEE 754-2008 a la citation intéressante suivante:

IEEE 754 § 10.4 Signification littérale et optimisations de changement de valeur

[...]

Les transformations de changement de valeur suivantes, entre autres, préservent la signification littérale du code source:

  • Application de la propriété d'identité 0 + x lorsque x n'est pas nul et n'est pas un NaN de signalisation et que le résultat a le même exposant que x.
  • Application de la propriété d'identité 1 × x lorsque x n'est pas un NaN de signalisation et que le résultat a le même exposant que x.
  • Modification de la charge utile ou du bit de signe d'un NaN silencieux.
  • [...]

Puisque tous les NaN et tous les infinis partagent le même exposant, et que le résultat correctement arrondi de x+0.0et x*1.0pour fini xa exactement la même grandeur que x, leur exposant est le même.

sNaNs

Les NaN de signalisation sont des valeurs d'interruption à virgule flottante; Ce sont des valeurs NaN spéciales dont l'utilisation comme opérande à virgule flottante entraîne une exception d'opération non valide (SIGFPE). Si une boucle qui déclenche une exception était optimisée, le logiciel ne se comporterait plus de la même manière.

Cependant, comme le fait remarquer user2357112 dans les commentaires , le standard C11 laisse explicitement indéfini le comportement de signalisation NaNs ( sNaN), de sorte que le compilateur est autorisé à supposer qu'ils ne se produisent pas, et donc que les exceptions qu'ils soulèvent ne se produisent pas non plus. Le standard C ++ 11 omet de décrire un comportement pour la signalisation des NaN, et le laisse donc également indéfini.

Modes d'arrondi

Dans les modes d'arrondi alternés, les optimisations autorisées peuvent changer. Par exemple, en mode Round-to-Negative-Infinity , l'optimisation x+0.0 -> xdevient admissible, mais x-0.0 -> xdevient interdite.

Pour éviter que GCC n'assume les modes et comportements d'arrondi par défaut, le drapeau expérimental -frounding-mathpeut être passé à GCC.

Conclusion

Clang et GCC , même à -O3, restent conformes à la norme IEEE-754. Cela signifie qu'il doit respecter les règles ci-dessus de la norme IEEE-754. x+0.0n'est pas un peu identique à xfor all xselon ces règles, mais x*1.0 peut être choisi comme tel : à savoir, lorsque nous

  1. Obéissez à la recommandation de transmettre inchangée la charge utile de xquand il s'agit d'un NaN.
  2. Laissez le bit de signe d'un résultat NaN inchangé par * 1.0.
  3. Obéissez à l'ordre de XOR le bit de signe pendant un quotient / produit, quand xn'est pas un NaN.

Pour activer l'optimisation IEEE-754-unsafe (x+0.0) -> x, l'indicateur -ffast-mathdoit être passé à Clang ou GCC.

Je n'existerai pas
la source
2
Attention: que se passe-t-il s'il s'agit d'un NaN de signalisation? (Je pensais en fait que c'était peut-être la raison, mais je ne savais pas vraiment comment, alors j'ai demandé.)
user541686
6
@Mehrdad: L'Annexe F, la partie (facultative) de la norme C qui spécifie l'adhésion C à IEEE 754, ne couvre pas explicitement les NaN de signalisation. (C11 F.2.1., Première ligne: "Cette spécification ne définit pas le comportement des NaN de signalisation.") Les implémentations qui déclarent la conformité à l'Annexe F restent libres de faire ce qu'elles veulent avec les NaN de signalisation. Le standard C ++ a sa propre gestion de l'IEEE 754, mais quoi qu'il en soit (je ne suis pas familier), je doute qu'il spécifie non plus le comportement de signalisation de NaN.
user2357112 prend en charge Monica
2
@Mehrdad: sNaN invoque un comportement non défini selon le standard (mais il est probablement bien défini par la plate-forme) donc le compilateur écrasant ici est autorisé.
Joshua
1
@ user2357112: La possibilité de piéger les erreurs comme effet secondaire pour des calculs autrement inutilisés interfère généralement avec beaucoup d'optimisation; si le résultat d'un calcul est parfois ignoré, un compilateur peut différer utilement le calcul jusqu'à ce qu'il sache si le résultat sera utilisé, mais si le calcul aurait produit un signal important, cela peut être mauvais.
supercat
2
Oh regardez, une question qui s'applique légitimement à la fois au C et au C ++ à laquelle il est répondu avec précision pour les deux langages par une référence à un seul standard. Cela rendra-t-il les gens moins susceptibles de se plaindre des questions marquées à la fois en C et en C ++, même lorsque la question concerne un langage commun? Malheureusement, je ne pense pas.
Kyle Strand
35

x += 0.0n'est pas un NOOP si xc'est -0.0. L'optimiseur pourrait de toute façon supprimer toute la boucle puisque les résultats ne sont pas utilisés, cependant. En général, il est difficile de dire pourquoi un optimiseur prend les décisions qu'il prend.

user2357112 prend en charge Monica
la source
2
En fait , je posté après avoir simplement lu pourquoi x += 0.0n'est pas un non-op, mais je pensais que c'est probablement pas la raison parce que la boucle entière doit être optimisé sur toute façon. Je peux l'acheter, ce n'est tout simplement pas aussi convaincant que je l'espérais ...
user541686
Étant donné la propension des langages orientés objet à produire des effets secondaires, j'imagine qu'il serait difficile d'être sûr que l'optimiseur ne change pas le comportement réel.
Robert Harvey
Cela pourrait être la raison, puisque long longl'optimisation est en vigueur (je l'ai fait avec gcc, qui se comporte de la même manière pour le double au moins)
e2-e4
2
@ ringø: long longest un type intégral, pas un type IEEE754.
MSalters
1
Et x -= 0, est-ce la même chose?
Viktor Mellgren