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 en C et C++ car je voudrais savoir si la réponse est différente pour chacun.)
La norme IEEE 754-2008 pour l'arithmétique en virgule flottante et la Norme ISO/IEC 10967 Arithmétique indépendante du langage (LIA), partie 1 expliquez pourquoi.
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'un seul NaN d'entrée, 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 l'exclusif OR des signes des opérandes; le signe d'une somme, ou d'une différence x - y considéré comme une somme x + (−y), diffère d'au plus l'un des signes des addends; 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 de signes opposés (ou la différence de deux opérandes de signes similaires) est exactement nulle, le signe de cette somme (ou différence) doit être +0 dans tous les attributs de direction d'arrondi sauf roundTowardNegative; sous cet attribut, le signe d'une somme (ou différence) nulle exacte doit être -0. Cependant, x + x = x −x) conserve le même signe que x même lorsque x est nul.
Sous le mode d'arrondi par défaut (Arrondi au plus proche, égal à égal), nous voyons que x+0.0
Produit x
, SAUF lorsque x
est -0.0
: Dans ce cas, nous avons une somme de deux opérandes de signes opposés dont la somme est nulle, et §6.3 le paragraphe 3 règle que cet ajout produit +0.0
.
Puisque +0.0
N'est pas au niveau du bit identique à l'original -0.0
, Et que -0.0
Est une valeur légitime qui peut se produire 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é: sous le mode d'arrondi par défaut, dans x+0.0
, Si x
-0.0
, alors x
lui-même est une valeur de sortie acceptable.-0.0
, puis la valeur de sortie doit être+0.0
, qui est pas au niveau du bit identique à -0.0
. Sous le mode d'arrondi par défaut , aucun problème de ce type ne se produit avec x*1.0
. Si x
:
x*1.0 == x
toujours.+/- infinity
, alors le résultat est +/- infinity
du même signe.est NaN
, puis selon
IEEE 754 § 6.2.3 Propagation NaN
Une opération qui propage un opérande NaN vers son résultat et a un seul NaN en entrée doit produire un NaN avec la charge utile du NaN d'entrée si elle est représentable dans le format de destination.
ce qui signifie que l'exposant et la mantisse (mais pas le signe) de NaN*1.0
sont recommandé pour être 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
.
+/- 0.0
, alors le résultat est un 0
avec son bit de signe XORed avec le bit de signe de 1.0
, en accord avec §6.3p2. Comme le bit de signe de 1.0
Est 0
, La valeur de sortie est inchangée par rapport à l'entrée. Ainsi, x*1.0 == x
Même lorsque x
est un zéro (négatif). Sous le mode d'arrondi par défaut , la soustraction x-0.0
Est également un no-op, car elle est équivalente à x + (-0.0)
. Si x
est
NaN
, alors les §6.3p1 et §6.2.3 s'appliquent de la même manière que pour l'addition et la multiplication.+/- infinity
, alors le résultat est +/- infinity
du même signe.x-0.0 == x
toujours.-0.0
, alors au §6.3p2 nous avons " [...] le signe d'une somme, ou d'une différence x - y considérée comme une somme x + (−y), diffère à partir d'au plus un des signes des compléments; ". Cela nous oblige à affecter -0.0
Comme résultat de (-0.0) + (-0.0)
, Car -0.0
Diffère en signe de aucun des addends, tandis que +0.0
Diffère en signe de deux des addends, en violation de cette clause.+0.0
, alors cela se réduit au cas d'addition (+0.0) + (-0.0)
considéré ci-dessus dans Le cas d'addition, qui en §6.3p3 est censé donner +0.0
.Étant donné que dans tous les cas, la valeur d'entrée est légale en tant que sortie, il est permis de considérer x-0.0
Comme non-op et x == x-0.0
Une tautologie.
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 modification de valeur suivantes, entre autres, préservent la signification littérale du code source:
- L'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.
- Appliquer 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 le résultat correctement arrondi de x+0.0
Et x*1.0
Pour fini x
a exactement la même amplitude que x
, leur exposant est le même.
Les NaN de signalisation sont des valeurs de piège à 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 user2357112 --- (souligne dans les commentaires, la norme C11 laisse explicitement indéfini le comportement de la signalisation des NaN (sNaN
), donc le compilateur est autorisés à supposer qu'elles ne se produisent pas, et donc que les exceptions qu'elles soulèvent ne se produisent pas non plus. Le standard C++ 11 omet de décrire un comportement de signalisation des NaN, et le laisse donc également indéfini.
Dans les autres modes d'arrondi, les optimisations autorisées peuvent changer. Par exemple, en mode Round-to-Negative-Infinity , l'optimisation x+0.0 -> x
Devient admissible, mais x-0.0 -> x
Devient interdit.
Pour empêcher GCC d'assumer les modes et comportements d'arrondi par défaut, l'indicateur expérimental -frounding-math
Peut être transmis à GCC.
Clang et GCC , même à -O3
, Reste conforme à la norme IEEE-754. Cela signifie qu'il doit respecter les règles ci-dessus de la norme IEEE-754. x+0.0
N'est pas pas identique à x
pour tous x
selon ces règles, mais x*1.0
peut être choisi pour être ainsi: À savoir, quand nous
x
lorsqu'il s'agit d'un NaN.* 1.0
.x
est pas un NaN.Pour activer l'optimisation IEEE-754 non sûre (x+0.0) -> x
, L'indicateur -ffast-math
Doit être transmis à Clang ou GCC.
x += 0.0
n'est pas un NOOP si x
est -0.0
. L'optimiseur pourrait de toute façon supprimer la boucle entière car les résultats ne sont pas utilisés. En général, il est difficile de dire pourquoi un optimiseur prend les décisions qu'il prend.