¿Por qué Clang optimiza el bucle en este código?
#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);
}
pero no el bucle en este código?
#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);
}
(Etiquetado como C y C ++ porque me gustaría saber si la respuesta es diferente para cada uno).
c++
c
optimization
floating-point
clang
usuario541686
fuente
fuente
-O3
, aunque no sé cómo comprobar qué se activa.static double arr[N]
no está permitido en C;const
las variables no cuentan como expresiones constantes en ese idiomaRespuestas:
El estándar IEEE 754-2008 para aritmética de coma flotante y el estándar de aritmética independiente del lenguaje (LIA) ISO / IEC 10967, Parte 1, responden por qué esto es así.
El caso de la suma
En el modo de redondeo predeterminado (Round-to-Nearest, Ties-to-Even) , vemos que
x+0.0
producex
, EXCEPTO cuandox
es-0.0
: En ese caso, tenemos una suma de dos operandos con signos opuestos cuya suma es cero, y §6.3 párrafo 3 reglas que produce esta adición+0.0
.Como
+0.0
no es idéntico en bits a la original-0.0
, y ese-0.0
es un valor legítimo que puede ocurrir como entrada, el compilador está obligado a poner el código que transformará los ceros negativos potenciales+0.0
.El resumen: en el modo de redondeo predeterminado, en
x+0.0
, six
-0.0
, entoncesx
sí mismo es un valor de salida aceptable.-0.0
, entonces el valor de salida debe ser+0.0
, que no es idéntico a nivel de bits-0.0
.El caso de la multiplicación
En el modo de redondeo predeterminado , no ocurre tal problema con
x*1.0
. Six
:x*1.0 == x
siempre.+/- infinity
, entonces el resultado es+/- infinity
del mismo signo.es
NaN
, entonces de acuerdo alo que significa que
NaN*1.0
se recomienda que el exponente y la mantisa (aunque no el signo) no se modifiquen desde la entradaNaN
. El signo no está especificado de acuerdo con §6.3p1 anterior, pero una implementación puede especificar que sea idéntico a la fuenteNaN
.+/- 0.0
, el resultado es un0
con su bit de signo XORed con el bit de signo de1.0
, de acuerdo con §6.3p2. Como el bit de signo de1.0
es0
, el valor de salida no cambia desde la entrada. Por lo tanto,x*1.0 == x
incluso cuandox
es un cero (negativo).El caso de la resta
En el modo de redondeo predeterminado , la resta
x-0.0
también es un no-op, porque es equivalente ax + (-0.0)
. Six
esNaN
, §6.3p1 y §6.2.3 se aplican de manera muy similar a la suma y multiplicación.+/- infinity
, entonces el resultado es+/- infinity
del mismo signo.x-0.0 == x
siempre.-0.0
, entonces, según §6.3p2 tenemos " el signo de una suma, o de una diferencia x - y considerada como una suma x + (−y), difiere de a lo sumo uno de los signos de los sumandos; ". Esto nos obliga a asignar-0.0
como resultado de(-0.0) + (-0.0)
, porque-0.0
difiere en el signo de ninguno de los sumandos, mientras que+0.0
difiere en el signo de dos de los sumandos, en violación de esta cláusula.+0.0
, esto se reduce al caso de adición(+0.0) + (-0.0)
considerado anteriormente en El caso de la adición , que según §6.3p3 se dicta que da+0.0
.Dado que para todos los casos el valor de entrada es legal como la salida, es permisible considerar
x-0.0
un no-op yx == x-0.0
una tautología.Optimizaciones de cambio de valor
El estándar IEEE 754-2008 tiene la siguiente cita interesante:
Dado que todos los NaN y todos los infinitos comparten el mismo exponente, y el resultado correctamente redondeado de
x+0.0
yx*1.0
para finitox
tiene exactamente la misma magnitud quex
, su exponente es el mismo.SNANS
Los NaN de señalización son valores de trampa de punto flotante; Son valores especiales de NaN cuyo uso como operando de punto flotante da como resultado una excepción de operación no válida (SIGFPE). Si se optimizara un bucle que desencadena una excepción, el software ya no se comportaría igual.
Sin embargo, como señala user2357112 en los comentarios , el Estándar C11 deja explícitamente indefinido el comportamiento de la señalización de NaNs (
sNaN
), por lo que el compilador puede suponer que no se producen y, por lo tanto, no se producen las excepciones que generan . El estándar C ++ 11 omite describir un comportamiento para la señalización de NaN y, por lo tanto, también lo deja indefinido.Modos de redondeo
En modos de redondeo alternativo, las optimizaciones permitidas pueden cambiar. Por ejemplo, en el modo Round-to-Negative-Infinity , la optimización se
x+0.0 -> x
vuelve permisible, perox-0.0 -> x
se prohíbe.Para evitar que GCC asuma modos y comportamientos de redondeo predeterminados, el indicador experimental
-frounding-math
se puede pasar a GCC.Conclusión
Clang y GCC , incluso en
-O3
, sigue cumpliendo con IEEE-754. Esto significa que debe cumplir con las reglas anteriores del estándar IEEE-754. nox+0.0
es idéntico ax
todos enx
virtud de esas reglas, perox*1.0
se puede elegir que sea así : a saber, cuandox
cuando es un NaN.* 1.0
.x
es un NaN.Para habilitar la optimización IEEE-754-insegura
(x+0.0) -> x
, la bandera-ffast-math
debe pasarse a Clang o GCC.fuente
x += 0.0
No es un NOOP six
es así-0.0
. Sin embargo, el optimizador podría eliminar todo el ciclo ya que los resultados no se utilizan. En general, es difícil saber por qué un optimizador toma las decisiones que toma.fuente
x += 0.0
no es un no-op, sin embargo, pensé que probablemente no es la razón porque todo el bucle debe ser optimizado a cabo de cualquier manera. Puedo comprarlo, simplemente no es tan convincente como esperaba ...long long
la optimización está en vigencia (lo hizo con gcc, que se comporta igual para el doble al menos)long long
es un tipo integral, no un tipo IEEE754.x -= 0
, es lo mismo?