¿Por qué Clang optimiza x * 1.0 pero NO x + 0.0?

 C Programming >> Programación C >  >> Tags >> Clang
¿Por qué Clang optimiza x * 1.0 pero NO x + 0.0?


¿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).


Respuestas:


El estándar IEEE 754-2008 para aritmética de coma flotante y el estándar ISO/IEC 10967 de aritmética independiente del lenguaje (LIA), parte 1, responden por qué esto es así.



El caso de la adición


En el modo de redondeo predeterminado (Redondo al más cercano, Empate a par) , vemos que x+0.0 produce x , EXCEPTO cuando x 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 rige que esta suma produce +0.0 .


Desde +0.0 no es bit a bit idéntico al -0.0 original , y que -0.0 es un valor legítimo que puede aparecer como entrada, el compilador está obligado a introducir el código que transformará los posibles ceros negativos en +0.0 .


El resumen:bajo el modo de redondeo predeterminado, en x+0.0 , si x



  • no es -0.0 , luego x en sí mismo es un valor de salida aceptable.

  • es -0.0 , entonces el valor de salida debe ser +0.0 , que no es bit a bit idéntico a -0.0 .


El caso de la multiplicación


En el modo de redondeo predeterminado , no ocurre tal problema con x*1.0 . Si x :



  • es un número (sub)normal, x*1.0 == x siempre.

  • es +/- infinity , entonces el resultado es +/- infinity del mismo signo.

  • es NaN , entonces según



    lo que significa que el exponente y la mantisa (aunque no el signo) de NaN*1.0 son recomendados permanecer sin cambios desde la entrada NaN . El signo no está especificado de acuerdo con §6.3p1 anterior, pero una implementación puede especificar que sea idéntico a la fuente NaN .


  • es +/- 0.0 , entonces el resultado es un 0 con su bit de signo XORed con el bit de signo de 1.0 , de acuerdo con §6.3p2. Desde el bit de signo de 1.0 es 0 , el valor de salida no cambia con respecto a la entrada. Por lo tanto, x*1.0 == x incluso cuando x es un cero (negativo).


El caso de la resta


En el modo de redondeo predeterminado , la resta x-0.0 es también un no-op, porque es equivalente a x + (-0.0) . Si x es



  • es NaN , entonces §6.3p1 y §6.2.3 se aplican de la misma manera que para la suma y la multiplicación.

  • es +/- infinity , entonces el resultado es +/- infinity del mismo signo.

  • es un número (sub)normal, x-0.0 == x siempre.

  • es -0.0 , entonces por §6.3p2 tenemos "[...] el signo de una suma, o de una diferencia x − y considerada como una suma x + (−y), difiere a lo sumo de uno de los sumandos' signos; ". Esto nos obliga a asignar -0.0 como resultado de (-0.0) + (-0.0) , porque -0.0 difiere en signo de ninguno de los sumandos, mientras que +0.0 difiere en signo de dos de los sumandos, en violación de esta cláusula.

  • es +0.0 , entonces 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 determina que da +0.0 .


Dado que para todos los casos el valor de entrada es válido como salida, está permitido considerar x-0.0 un no-op, y x == 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 redondeado correctamente de x+0.0 y x*1.0 para finito x tiene exactamente la misma magnitud que x , 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 activa una excepción, el software ya no se comportaría igual.


Sin embargo, como el usuario2357112 señala en los comentarios , el estándar C11 deja explícitamente sin definir el comportamiento de los NaN de señalización (sNaN ), por lo que el compilador puede asumir que no ocurren y, por lo tanto, que las excepciones que generan tampoco ocurren. El estándar C++11 omite describir un comportamiento para señalar NaN y, por lo tanto, también lo deja sin definir.


Modos de redondeo


En modos de redondeo alternativos, las optimizaciones permitidas pueden cambiar. Por ejemplo, en Round-to-Negative-Infinity modo, la optimización x+0.0 -> x se vuelve permisible, pero x-0.0 -> x queda prohibido.


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 siendo compatible con IEEE-754. Esto significa que debe cumplir con las reglas anteriores del estándar IEEE-754. x+0.0 es no idéntico en bits a x para todos x bajo esas reglas, pero x*1.0 puede ser elegido para serlo :Es decir, cuando



  1. Obedecer la recomendación de pasar sin cambios la carga útil de x cuando es un NaN.

  2. Deje el bit de signo de un resultado NaN sin cambios por * 1.0 .

  3. Obedecer la orden de XOR el bit de signo durante un cociente/producto, cuando x es no un NaN.


Para habilitar la optimización insegura IEEE-754 (x+0.0) -> x , la bandera -ffast-math debe pasarse a Clang o GCC.


Algunas respuestas de 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);
}
#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);
}