Tengo cuatro funciones de identidad que esencialmente no hacen nada. Solo la multiplicación con 1 podría optimizarse haciendo sonar una sola instrucción ret .
float id0(float x) { return x + 1 - 1; } float id1(float x) { return x + 0; } float id2(float x) { return x * 2 / 2; } float id3(float x) { return x * 1; }Y la siguiente salida del compilador es: (clang 10, at -O3)
.LCPI0_0: .long 1065353216 # float 1 .LCPI0_1: .long 3212836864 # float -1 id0(float): # @id0(float) addss xmm0, dword ptr [rip + .LCPI0_0] addss xmm0, dword ptr [rip + .LCPI0_1] ret id1(float): # @id1(float) xorps xmm1, xmm1 addss xmm0, xmm1 ret .LCPI2_0: .long 1056964608 # float 0.5 id2(float): # @id2(float) addss xmm0, xmm0 mulss xmm0, dword ptr [rip + .LCPI2_0] ret id3(float): # @id3(float) ret Puedo entender por qué id0 e id2 no se pueden optimizar. Aumentan el valor que luego podría convertirse en infinito positivo y la segunda operación no lo volvería a cambiar.
Pero, ¿por qué no se puede optimizar id1 ? La suma con infinito daría infinito, la suma con cualquier número regular daría ese número y la suma con NaN daría NaN . Entonces, ¿por qué no es una operación de identidad "verdadera" como * 1 ?
Los números de punto flotante IEEE 754 tienen dos valores cero, uno negativo y otro positivo. Cuando se suman, el resultado es positivo.
Entonces id1(-0.f) es 0.f , no -0.f .
Tenga en cuenta que id1(-0.f) == -0.f porque 0.f == -0.f .
Además, tenga en cuenta que compilar con -ffast-math en GCC realiza la optimización y cambia el resultado.
"Tengo cuatro funciones de identidad que esencialmente no hacen nada".
Eso no es cierto.
Para números de punto flotante x + 1 - 1 no es igual a x + 0 , es igual a (x + 1) - 1 . Entonces, si tiene, por ejemplo, una x muy pequeña, perderá esa porción muy pequeña en el paso x + 1 , y el compilador no puede saber si esa fue su intención o no.
Y en el caso de x * 2 / 2 , es posible que x * 2 tampoco sea exacto, debido a la precisión del punto flotante, por lo que tiene un caso similar aquí, el compilador no sabe si por alguna razón desea cambiar el valor de x de esa manera.
Entonces estos serían iguales:
float id0(float x) { return x + (1. - 1.); } float id1(float x) { return x + 0; }Y estos serían iguales:
float id2(float x) { return x * (2. / 2.); } float id3(float x) { return x * 1; } El comportamiento deseado seguramente podría definirse de otra manera. Pero como ya mencionó Nelfeal, esta optimización debe activarse explícitamente usando -ffast-math
Habilite el modo matemático rápido. Esta opción le permite al compilador hacer suposiciones agresivas y potencialmente con pérdidas sobre las matemáticas de punto flotante. Éstos incluyen:
- Las matemáticas de punto flotante obedecen reglas algebraicas regulares para números reales (p. ej., + y * son asociativos, x/y == x * (1/y) y (a + b) * c == a * c + b * c) ,
- Los operandos de las operaciones de coma flotante no son iguales a NaN e Inf, y
- +0 y -0 son intercambiables.
fast-math es para clang y gcc una colección de banderas (aquí la listada por clang):
- -fno-honor-infinitos
- -fno-honor-nans
- -fno-math-errno
- -matemáticas finitas
- -matemáticas fasociativas
- -matemáticas-freciprocas
- -fno-ceros con signo
- -fno-trapping-matemáticas
- -ffp-contrato=rápido
Lea la página web float-number-gui.de , más información sobre IEEE 754 , el estándar C11 n1570 , el estándar C++11 n3337 .
float id1(float x) { return x + 0; } Si x resulta ser un NaN de señalización, es posible que su id1 incluso no regrese (y probablemente no debería regresar).
Si x es un NaN silencioso, entonces id1(x) != x ya que NaN != NaN (al menos NaN == NaN debería ser falso).
En algunos casos, desea aritmética de precisión arbitraria costosa. Entonces considere usar GMPlib .
PD. Los números de coma flotante pueden provocarle pesadillas o un doctorado, a su elección. A veces matan a personas o al menos provocan grandes desastres financieros (por ejemplo, una pérdida de varios cientos de millones de dólares estadounidenses o euros).