¿Por qué GCC no optimiza a * a * a * a * a * a to (a * a * a) * (a * a * a)?

2120

Estoy haciendo una optimización numérica en una aplicación científica. Una cosa que noté es que GCC optimizará la llamada pow(a,2)compilándola a*a, pero la llamada pow(a,6)no está optimizada y realmente llamará a la función de biblioteca pow, lo que ralentiza enormemente el rendimiento. (En contraste, el compilador Intel C ++ , ejecutable icc, eliminará la llamada a la biblioteca pow(a,6)).

Lo que me interesa es que cuando lo reemplacé pow(a,6)con a*a*a*a*a*aGCC 4.5.1 y las opciones " -O3 -lm -funroll-loops -msse4", utiliza 5 mulsdinstrucciones:

movapd  %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13

mientras que si escribo (a*a*a)*(a*a*a), producirá

movapd  %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm13, %xmm13

lo que reduce el número de instrucciones de multiplicación a 3. icctiene un comportamiento similar.

¿Por qué los compiladores no reconocen este truco de optimización?

xis
fuente
13
¿Qué significa "reconocer pow (a, 6)"?
Varun Madiath
659
Um ... sabes que a a a a a a y (a a a) * (a a * a) no son lo mismo con números de coma flotante, ¿no? Tendrás que usar -funsafe-math o -ffast-math o algo para eso.
Damon
106
Le sugiero que lea "Lo que todo informático debe saber sobre la aritmética de coma flotante" por David Goldberg: download.oracle.com/docs/cd/E19957-01/806-3568/… después de lo cual tendrá una comprensión más completa de ¡El pozo de alquitrán al que acabas de entrar!
Phil Armstrong
189
Una pregunta perfectamente razonable. Hace 20 años hice la misma pregunta general, y al aplastar ese cuello de botella, reduje el tiempo de ejecución de una simulación de Monte Carlo de 21 horas a 7 horas. El código en el bucle interno se ejecutó 13 billones de veces en el proceso, pero consiguió la simulación en una ventana nocturna. (vea la respuesta a continuación)
23
Tal vez tirar (a*a)*(a*a)*(a*a)a la mezcla, también. El mismo número de multiplicaciones, pero probablemente más preciso.
Rok Kralj

Respuestas:

2738

Porque Floating Point Math no es asociativo . La forma en que agrupa los operandos en la multiplicación de coma flotante tiene un efecto en la precisión numérica de la respuesta.

Como resultado, la mayoría de los compiladores son muy conservadores al reordenar los cálculos de coma flotante a menos que puedan estar seguros de que la respuesta seguirá siendo la misma, o a menos que les diga que no le importa la precisión numérica. Por ejemplo: la -fassociative-mathopción de gcc que le permite a gcc reasociar operaciones de coma flotante, o incluso la -ffast-mathopción que permite compensaciones aún más agresivas de precisión contra velocidad.

Lambdageek
fuente
10
Si. Con -ffast-math está haciendo tal optimización. ¡Buena idea! Pero dado que nuestro código se refiere a más precisión que la velocidad, podría ser mejor no pasarlo.
x es
19
IIRC C99 permite que el compilador realice optimizaciones FP "inseguras", pero GCC (en cualquier otra cosa que no sea el x87) hace un intento razonable de seguir IEEE 754 - no son "límites de error"; Solo hay una respuesta correcta .
tc.
14
Los detalles de implementación de powno están aquí ni allá; Esta respuesta ni siquiera hace referencia pow.
Stephen Canon
14
@nedR: ICC por defecto permite permitir la nueva asociación. Si desea obtener un comportamiento de conformidad estándar, debe configurarlo -fp-model precisecon ICC. clangy por gccdefecto a estricta conformidad wrt reassociation.
Stephen Canon
49
@xis, no es realmente eso -fassociative-mathsería inexacto; es solo eso a*a*a*a*a*ay (a*a*a)*(a*a*a)son diferentes. No se trata de precisión; Se trata de la conformidad de estándares y resultados estrictamente repetibles, por ejemplo, los mismos resultados en cualquier compilador. Los números de coma flotante ya no son exactos. Raramente es inapropiado compilar con -fassociative-math.
Paul Draper
652

Lambdageek señala correctamente que debido a que la asociatividad no se cumple para los números de coma flotante, la "optimización" dea*a*a*a*a*ato(a*a*a)*(a*a*a)puede cambiar el valor. Es por eso que C99 no lo permite (a menos que el usuario lo permita específicamente, a través del indicador del compilador o pragma). En general, se supone que el programador escribió lo que hizo por una razón, y el compilador debería respetar eso. Si quieres(a*a*a)*(a*a*a), escribe eso.

Sin embargo, puede ser un dolor de escribir; ¿Por qué el compilador no puede hacer [lo que consideras que es] lo correcto cuando lo usas pow(a,6)? Porque sería lo incorrecto hacer. En una plataforma con una buena biblioteca de matemáticas, pow(a,6)es significativamente más preciso que sea a*a*a*a*a*ao (a*a*a)*(a*a*a). Solo para proporcionar algunos datos, realicé un pequeño experimento en mi Mac Pro, midiendo el peor error al evaluar un ^ 6 para todos los números flotantes de precisión simple entre [1,2):

worst relative error using    powf(a, 6.f): 5.96e-08
worst relative error using (a*a*a)*(a*a*a): 2.94e-07
worst relative error using     a*a*a*a*a*a: 2.58e-07

Usar en powlugar de un árbol de multiplicación reduce el error limitado por un factor de 4 . Los compiladores no deben (y generalmente no lo hacen) "optimizaciones" que aumenten el error a menos que el usuario lo autorice (por ejemplo, a través de -ffast-math).

Tenga en cuenta que GCC proporciona __builtin_powi(x,n)como alternativa a pow( ), que debería generar un árbol de multiplicación en línea. Úselo si desea cambiar la precisión por el rendimiento, pero no desea habilitar las matemáticas rápidas.

Stephen Canon
fuente
29
Tenga en cuenta también que Visual C ++ proporciona una versión 'mejorada' de pow (). Al llamar _set_SSE2_enable(<flag>)con flag=1, utilizará SSE2 si es posible. Esto reduce la precisión un poco, pero mejora la velocidad (en algunos casos). MSDN: _set_SSE2_enable () y pow ()
TkTech
18
@TkTech: cualquier precisión reducida se debe a la implementación de Microsoft, no al tamaño de los registros utilizados. Es posible entregar un redondeo correcto pow utilizando solo registros de 32 bits, si el escritor de la biblioteca está tan motivado. Hay powimplementaciones basadas en SSE que son más precisas que la mayoría de las implementaciones basadas en x87, y también hay implementaciones que intercambian cierta precisión por la velocidad.
Stephen Canon
99
@TkTech: Por supuesto, solo quería dejar en claro que la reducción en la precisión se debe a las elecciones hechas por los escritores de la biblioteca, no intrínseca al uso de SSE.
Stephen Canon
77
Me interesa saber qué usó como el "estándar de oro" aquí para calcular los errores relativos. Normalmente hubiera esperado que fuera así a*a*a*a*a*a, ¡pero aparentemente ese no es el caso! :)
j_random_hacker
8
@j_random_hacker: dado que estaba comparando resultados de precisión simple, la precisión doble es suficiente para un estándar de oro: el error de un a a a a a calculado en doble * es mucho más pequeño que el error de cualquiera de los cálculos de precisión simple.
Stephen Canon
168

Otro caso similar: la mayoría de los compiladores no optimiza a + b + c + da (a + b) + (c + d)(esto es una optimización desde la segunda expresión se puede pipeline mejor) y evaluarlo como dado (es decir, como (((a + b) + c) + d)). Esto también se debe a casos de esquina:

float a = 1e35, b = 1e-5, c = -1e35, d = 1e-5;
printf("%e %e\n", a + b + c + d, (a + b) + (c + d));

Esto salidas 1.000000e-05 0.000000e+00

sanjoyd
fuente
10
Esto no es exactamente lo mismo. Changin el orden de multiplicaciones / divisiones (excluyendo la división por 0) es más seguro que el orden de cambio de suma / resta. En mi humilde opinión, el compilador debería intentar asociar mults./divs. porque hacer eso reduce el número total de operaciones y además de la ganancia de rendimiento también hay una ganancia de precisión.
CoffeDeveloper
44
@DarioOO: No es más seguro. Multiplicar y dividir son lo mismo que sumar y restar el exponente, y cambiar el orden fácilmente puede causar que los temporales excedan el rango posible del exponente. (No es exactamente lo mismo, porque el exponente no sufre pérdida de precisión ... pero la representación sigue siendo bastante limitada, y la reordenación puede conducir a valores no representables)
Ben Voigt
8
Creo que te faltan algunos antecedentes de cálculo. Multiplicar y dividir 2 números introduce la misma cantidad de error. Si bien restar / sumar 2 números puede introducir un error mayor, especialmente cuando los 2 números tienen un orden de magnitud diferente, por lo tanto, es más seguro reorganizar mul / divide que sub / add porque introduce un cambio menor en el error final.
CoffeDeveloper
8
@DarioOO: el riesgo es diferente con mul / div: la reordenación hace un cambio insignificante en el resultado final o el exponente se desborda en algún momento (donde no lo habría hecho antes) y el resultado es enormemente diferente (potencialmente + inf o 0).
Peter Cordes
@GameDeveloper Imponer una ganancia de precisión de maneras impredecibles es muy problemático.
curioso
80

Fortran (diseñado para computación científica) tiene un operador de potencia incorporado, y que yo sepa, los compiladores de Fortran comúnmente optimizarán la elevación a potencias enteras de manera similar a lo que usted describe. Desafortunadamente, C / C ++ no tiene un operador de energía, solo la función de biblioteca pow(). Esto no evita que los compiladores inteligentes lo traten powespecialmente y lo computen de una manera más rápida para casos especiales, pero parece que lo hacen con menos frecuencia ...

Hace algunos años, estaba tratando de hacer más conveniente el cálculo de potencias enteras de manera óptima, y ​​se me ocurrió lo siguiente. Sin embargo, es C ++, no C, y aún depende de que el compilador sea algo inteligente sobre cómo optimizar / alinear las cosas. De todos modos, espero que les resulte útil en la práctica:

template<unsigned N> struct power_impl;

template<unsigned N> struct power_impl {
    template<typename T>
    static T calc(const T &x) {
        if (N%2 == 0)
            return power_impl<N/2>::calc(x*x);
        else if (N%3 == 0)
            return power_impl<N/3>::calc(x*x*x);
        return power_impl<N-1>::calc(x)*x;
    }
};

template<> struct power_impl<0> {
    template<typename T>
    static T calc(const T &) { return 1; }
};

template<unsigned N, typename T>
inline T power(const T &x) {
    return power_impl<N>::calc(x);
}

Aclaración para los curiosos: esto no encuentra la forma óptima de calcular las potencias, pero dado que encontrar la solución óptima es un problema NP-completo y esto solo vale la pena hacerlo para las potencias pequeñas de todos modos (en lugar de usar pow), no hay razón para preocuparse Con el detalle.

Entonces solo úsalo como power<6>(a).

Esto facilita la escritura de poderes (no es necesario deletrear 6 as con parens), y le permite tener este tipo de optimización sin -ffast-mathtener que depender de la precisión, como la suma compensada (un ejemplo donde el orden de las operaciones es esencial) .

Probablemente también pueda olvidar que se trata de C ++ y simplemente usarlo en el programa C (si se compila con un compilador de C ++).

Espero que esto pueda ser útil.

EDITAR:

Esto es lo que obtengo de mi compilador:

para a*a*a*a*a*a,

    movapd  %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0

para (a*a*a)*(a*a*a),

    movapd  %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm0, %xmm0

para power<6>(a),

    mulsd   %xmm0, %xmm0
    movapd  %xmm0, %xmm1
    mulsd   %xmm0, %xmm1
    mulsd   %xmm0, %xmm1
Szabolcs
fuente
36
Encontrar el árbol de potencia óptimo puede ser difícil, pero dado que solo es interesante para las potencias pequeñas, la respuesta obvia es precalcularlo una vez (Knuth proporciona una tabla de hasta 100) y usar esa tabla codificada (eso es lo que hace gcc internamente para powi) .
Marc Glisse
77
En los procesadores modernos, la velocidad está limitada por la latencia. Por ejemplo, el resultado de una multiplicación podría estar disponible después de cinco ciclos. En esa situación, encontrar la forma más rápida de crear algo de poder podría ser más complicado.
gnasher729
3
También podría intentar encontrar el árbol de potencia que proporciona el límite superior más bajo para el error de redondeo relativo, o el error de redondeo relativo promedio más bajo.
gnasher729
1
Boost también tiene soporte para esto, por ejemplo: boost :: math :: pow <6> (n); Creo que incluso trata de reducir el número de multiplicaciones mediante la extracción de factores comunes.
gast128
Tenga en cuenta que el último es equivalente a (a ** 2) ** 3
minmaxavg
62

GCC realmente se optimiza a*a*a*a*a*apara (a*a*a)*(a*a*a)cuando a es un número entero. Intenté con este comando:

$ echo 'int f(int x) { return x*x*x*x*x*x; }' | gcc -o - -O2 -S -masm=intel -x c -

Hay muchas banderas de gcc pero nada lujoso. Significan: leer de stdin; utilizar el nivel de optimización de O2; salida del listado del lenguaje ensamblador en lugar de un binario; la lista debe usar la sintaxis del lenguaje ensamblador Intel; la entrada está en lenguaje C (generalmente el idioma se infiere de la extensión del archivo de entrada, pero no hay extensión de archivo cuando se lee desde stdin); y escribe a stdout.

Aquí está la parte importante de la salida. Lo he anotado con algunos comentarios que indican lo que está sucediendo en el lenguaje ensamblador:

; x is in edi to begin with.  eax will be used as a temporary register.
mov  eax, edi  ; temp = x
imul eax, edi  ; temp = x * temp
imul eax, edi  ; temp = x * temp
imul eax, eax  ; temp = temp * temp

Estoy usando el sistema GCC en Linux Mint 16 Petra, un derivado de Ubuntu. Aquí está la versión de gcc:

$ gcc --version
gcc (Ubuntu/Linaro 4.8.1-10ubuntu9) 4.8.1

Como han señalado otros carteles, esta opción no es posible en coma flotante, porque la aritmética de coma flotante no es asociativa.

picomancer
fuente
12
Esto es legal para la multiplicación de enteros porque el desbordamiento del complemento a dos es un comportamiento indefinido. Si va a haber un desbordamiento, sucederá en algún lugar, independientemente de las operaciones de reordenamiento. Entonces, las expresiones sin desbordamiento evalúan lo mismo, las expresiones que desbordan son comportamientos indefinidos, por lo que está bien que el compilador cambie el punto en el que ocurre el desbordamiento. gcc también hace esto con unsigned int.
Peter Cordes
51

Porque un número de coma flotante de 32 bits, como 1.024, no es 1.024. En una computadora, 1.024 es un intervalo: de (1.024-e) a (1.024 + e), donde "e" representa un error. Algunas personas no se dan cuenta de esto y también creen que * en a * a representa la multiplicación de números de precisión arbitraria sin que haya ningún error asociado a esos números. La razón por la cual algunas personas no se dan cuenta de esto es quizás los cálculos matemáticos que ejercieron en las escuelas primarias: trabajar solo con números ideales sin errores adjuntos y creer que está bien simplemente ignorar "e" mientras se realiza la multiplicación. No ven la "e" implícita en "float a = 1.2", "a * a * a" y códigos C similares.

Si la mayoría de los programadores reconocen (y pueden ejecutar) la idea de que la expresión C a * a * a * a * a * a en realidad no funciona con números ideales, el compilador GCC sería GRATIS para optimizar "a * a * a * a * a * a "en decir" t = (a * a); t * t * t "que requiere un número menor de multiplicaciones. Pero desafortunadamente, el compilador de GCC no sabe si el programador que escribe el código piensa que "a" es un número con o sin error. Y así, GCC solo hará lo que parece el código fuente, porque eso es lo que GCC ve a simple vista.

... una vez que sepa qué tipo de programador que se encuentre, puede utilizar el interruptor "-ffast-matemáticas" para decirle a gcc que "Hey, GCC, sé lo que estoy haciendo!". Esto permitirá que GCC convierta a * a * a * a * a * a en un texto diferente; se ve diferente de a * a * a * a * a * a, pero aún calcula un número dentro del intervalo de error de a * a * a * a * a * a. Esto está bien, ya que ya sabe que está trabajando con intervalos, no con números ideales.


fuente
52
Los números de coma flotante son exactos. No son necesariamente exactamente lo que esperabas. Además, la técnica con épsilon es en sí misma una aproximación a cómo abordar las cosas en la realidad, porque el verdadero error esperado es relativo a la escala de la mantisa, es decir, normalmente está a aproximadamente 1 LSB, pero eso puede aumentar con cada operación se realiza si no tiene cuidado, consulte a un analista numérico antes de hacer algo no trivial con coma flotante. Use una biblioteca adecuada si es posible.
Donal Fellows
3
@DonalFellows: el estándar IEEE requiere que los cálculos de punto flotante produzcan el resultado que coincida más exactamente con el resultado si los operandos de origen fueran valores exactos, pero eso no significa que realmente representen valores exactos. En muchos casos es más útil considerar 0.1f como (1,677,722 +/- 0.5) / 16,777,216, que debe mostrarse con el número de dígitos decimales implicados por esa incertidumbre, que considerarlo como una cantidad exacta (1,677,722 +/- 0.5) / 16,777,216 (que debe mostrarse con 24 dígitos decimales).
supercat
23
@supercat: IEEE-754 es bastante claro sobre el punto de que los datos de punto flotante hacen representar valores exactos; Las cláusulas 3.2 - 3.4 son las secciones relevantes. Puede, por supuesto, elegir interpretarlos de otra manera, así como puede elegir interpretar int x = 3como significado que xes 3 +/- 0.5.
Stephen Canon
77
@supercat: estoy totalmente de acuerdo, pero eso no significa que Distanceno sea exactamente igual a su valor numérico; significa que el valor numérico es solo una aproximación a alguna cantidad física que se está modelando.
Stephen Canon
10
Para el análisis numérico, su cerebro se lo agradecerá si interpreta los números de coma flotante no como intervalos, sino como valores exactos (que no son exactamente los valores que deseaba). Por ejemplo, si x está en algún lugar alrededor de 4.5 con un error menor que 0.1, y usted calcula (x + 1) - x, la interpretación del "intervalo" lo deja con un intervalo de 0.8 a 1.2, mientras que la interpretación del "valor exacto" indica el resultado será 1 con un error de 2 ^ (- 50) como máximo en doble precisión.
gnasher729
34

Ningún póster ha mencionado todavía la contracción de las expresiones flotantes (estándar ISO C, 6.5p8 y 7.12.2). Si el FP_CONTRACTpragma se establece en ON, el compilador puede considerar una expresión a*a*a*a*a*acomo una sola operación, como si se evaluara exactamente con un solo redondeo. Por ejemplo, un compilador puede reemplazarlo por una función de potencia interna que es más rápida y más precisa. Esto es particularmente interesante ya que el comportamiento está parcialmente controlado por el programador directamente en el código fuente, mientras que las opciones del compilador proporcionadas por el usuario final a veces se pueden usar incorrectamente.

El estado predeterminado del FP_CONTRACTpragma está definido por la implementación, de modo que un compilador puede realizar tales optimizaciones de manera predeterminada. Por lo tanto, el código portátil que debe seguir estrictamente las reglas IEEE 754 debe establecerlo explícitamente OFF.

Si un compilador no admite este pragma, debe ser conservador evitando dicha optimización, en caso de que el desarrollador haya elegido configurarlo OFF.

GCC no admite este pragma, pero con las opciones predeterminadas, asume que es así ON; por lo tanto, para objetivos con un FMA de hardware, si se quiere evitar la transformación a*b+ca fma (a, b, c), se debe proporcionar una opción como -ffp-contract=off(establecer explícitamente el pragma en OFF) o -std=c99(para indicarle a GCC que se ajuste a algunos Versión estándar C, aquí C99, por lo tanto, siga el párrafo anterior). En el pasado, la última opción no impedía la transformación, lo que significa que GCC no se ajustaba a este punto: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=37845

vinc17
fuente
3
Las preguntas populares de larga duración a veces muestran su edad. Esta pregunta fue formulada y respondida en 2011, cuando GCC podría ser excusado por no respetar exactamente el estándar C99 reciente. Por supuesto, ahora es 2014, así que GCC ... ejem.
Pascal Cuoq
Sin embargo, ¿no deberías responder preguntas de punto flotante relativamente recientes sin una respuesta aceptada? tos stackoverflow.com/questions/23703408 tos
Pascal Cuoq
Me parece ... inquietante que gcc no implemente pragmas de punto flotante C99.
David Monniaux el
1
Los pragmas de @DavidMonniaux son, por definición, opcionales de implementar.
Tim Seguine
2
@TimSeguine Pero si no se implementa un pragma, su valor predeterminado debe ser el más restrictivo para la implementación. Supongo que en eso estaba pensando David. Con GCC, esto ahora se arregla para FP_CONTRACT si se usa un modo ISO C : todavía no implementa el pragma, pero en un modo ISO C, ahora se supone que el pragma está apagado.
vinc17
28

Como Lambdageek señaló, la multiplicación por flotación no es asociativa y se puede obtener menos precisión, pero también cuando se obtiene una mayor precisión se puede argumentar en contra de la optimización, porque se desea una aplicación determinista. Por ejemplo, en la simulación de juego cliente / servidor, donde cada cliente tiene que simular el mismo mundo en el que desea que los cálculos de coma flotante sean deterministas.

Bjorn
fuente
3
@greggo No, todavía es determinista entonces. No se agrega aleatoriedad en ningún sentido de la palabra.
Alice
99
@Alice Parece bastante claro que Bjorn aquí está usando 'determinista' en el sentido del código que da el mismo resultado en diferentes plataformas y diferentes versiones del compilador, etc. (variables externas que pueden estar fuera del control del programador), en lugar de falta de aleatoriedad numérica real en tiempo de ejecución. Si está señalando que este no es un uso adecuado de la palabra, no voy a discutir eso.
greggo
55
@greggo Excepto incluso en su interpretación de lo que dice, todavía está mal; ese es el objetivo de IEEE 754, proporcionar características idénticas para la mayoría (si no todas) las operaciones en todas las plataformas. Ahora, no mencionó las plataformas o las versiones del compilador, lo que sería una preocupación válida si desea que cada operación en cada servidor / cliente remoto sea idéntica ... pero esto no es obvio en su declaración. Una palabra mejor podría ser "confiablemente similar" o algo así.
Alice
8
@Alice estás perdiendo el tiempo de todos, incluido el tuyo, discutiendo la semántica. Su significado era claro.
Lanaru
11
@Lanaru El punto completo de las normas es la semántica; su significado definitivamente no estaba claro.
Alice
28

Las funciones de la biblioteca como "pow" generalmente están cuidadosamente diseñadas para producir el mínimo error posible (en caso genérico). Esto generalmente se logra aproximando funciones con splines (según el comentario de Pascal, la implementación más común parece estar usando el algoritmo Remez )

fundamentalmente la siguiente operación:

pow(x,y);

tiene un error inherente de aproximadamente la misma magnitud que el error en cualquier multiplicación o división .

Mientras que la siguiente operación:

float a=someValue;
float b=a*a*a*a*a*a;

tiene un error inherente que es mayor que 5 veces el error de una sola multiplicación o división (porque está combinando 5 multiplicaciones).

El compilador debe tener mucho cuidado con el tipo de optimización que está haciendo:

  1. Si la optimización pow(a,6)de a*a*a*a*a*aque puede mejorar el rendimiento, pero reducir drásticamente la exactitud de los números de punto flotante.
  2. si la optimización a*a*a*a*a*a a pow(a,6)que en realidad puede reducir la precisión porque "a" era algún valor especial que permite la multiplicación sin error (una potencia de 2 o un número entero pequeño)
  3. Si la optimización pow(a,6)de (a*a*a)*(a*a*a)o (a*a)*(a*a)*(a*a)que todavía puede ser una pérdida de precisión en comparación con powla función.

En general, usted sabe que para valores arbitrarios de coma flotante "pow" tiene mejor precisión que cualquier función que eventualmente podría escribir, pero en algunos casos especiales, las multiplicaciones múltiples pueden tener una mejor precisión y rendimiento, depende del desarrollador elegir qué es más apropiado, eventualmente comentando el código para que nadie más "optimice" ese código.

Lo único que tiene sentido (opinión personal, y aparentemente una elección en GCC sin ninguna optimización particular o indicador de compilación) para optimizar debería ser reemplazar "pow (a, 2)" con "a * a". Eso sería lo único sensato que un vendedor de compiladores debería hacer.

CoffeDeveloper
fuente
77
los votantes negativos deben darse cuenta de que esta respuesta está perfectamente bien. Puedo citar docenas de fuentes y documentos para respaldar mi respuesta y probablemente estoy más involucrado con la precisión de coma flotante que cualquier otro votante. Es perfectamente razonable en StackOverflow agregar información faltante que otras respuestas no cubren, así que sea cortés y explique sus razones.
CoffeDeveloper
1
Me parece que la respuesta de Stephen Canon cubre lo que tienes que decir. Parece que insiste en que los libms se implementen con splines: normalmente usan la reducción de argumentos (dependiendo de la función que se esté implementando) más un polinomio único cuyos coeficientes han sido obtenidos por variantes más o menos sofisticadas del algoritmo Remez. La suavidad en los puntos de unión no se considera un objetivo que valga la pena perseguir para las funciones de libm (si terminan con la precisión suficiente, de todos modos son automáticamente bastante suaves independientemente de en cuántas partes se dividió el dominio).
Pascal Cuoq
La segunda mitad de su respuesta pierde por completo el punto de que se supone que los compiladores producen código que implementa lo que dice el código fuente, punto. También utiliza la palabra "precisión" cuando quiere decir "precisión".
Pascal Cuoq
Gracias por su aporte, he corregido ligeramente la respuesta, algo nuevo todavía está presente en las últimas 2 líneas ^^
CoffeDeveloper
27

No hubiera esperado que este caso se optimizara en absoluto. No puede ser muy frecuente que una expresión contenga subexpresiones que pueden reagruparse para eliminar operaciones completas. Esperaría que los escritores de compiladores inviertan su tiempo en áreas que tendrían más probabilidades de generar mejoras notables, en lugar de cubrir un caso marginal que rara vez se encuentra.

Me sorprendió saber de las otras respuestas que esta expresión podría optimizarse con los modificadores de compilador adecuados. O la optimización es trivial, o es un caso extremo de una optimización mucho más común, o los escritores del compilador fueron extremadamente minuciosos.

No hay nada de malo en proporcionar pistas al compilador como lo ha hecho aquí. Es una parte normal y esperada del proceso de microoptimización reorganizar las declaraciones y expresiones para ver qué diferencias traerán.

Si bien el compilador puede estar justificado al considerar las dos expresiones para entregar resultados inconsistentes (sin los modificadores adecuados), no es necesario que esté sujeto a esa restricción. La diferencia será increíblemente pequeña, tanto que si la diferencia es importante para usted, no debería usar la aritmética de coma flotante estándar en primer lugar.

Mark Ransom
fuente
17
Como señaló otro comentarista, esto no es cierto hasta el punto de ser absurdo; la diferencia podría ser tanto como la mitad al 10% del costo, y si se ejecuta en un ciclo cerrado, eso se traducirá en muchas instrucciones desperdiciadas para obtener lo que podría ser una cantidad insignificante de precisión adicional. Decir que no debes usar FP estándar cuando estás haciendo un monte carlo es como decir que siempre debes usar un avión para cruzar el país; Ignora muchas externalidades. Finalmente, esto NO es una optimización poco común; El análisis de código muerto y la reducción / refactorización de código es muy común.
Alice
21

Ya hay algunas buenas respuestas a esta pregunta, pero en aras de la exhaustividad, quería señalar que la sección correspondiente del estándar C es 5.1.2.2.3 / 15 (que es lo mismo que la sección 1.9 / 9 en el C ++ 11 estándar). Esta sección establece que los operadores solo pueden reagruparse si son realmente asociativos o conmutativos.

Rastaban
fuente
12

En realidad, gcc puede hacer esta optimización, incluso para números de punto flotante. Por ejemplo,

double foo(double a) {
  return a*a*a*a*a*a;
}

se convierte

foo(double):
    mulsd   %xmm0, %xmm0
    movapd  %xmm0, %xmm1
    mulsd   %xmm0, %xmm1
    mulsd   %xmm1, %xmm0
    ret

con -O -funsafe-math-optimizations. Sin embargo, este reordenamiento viola IEEE-754, por lo que requiere la bandera.

Los enteros firmados, como señaló Peter Cordes en un comentario, pueden hacer esta optimización sin, -funsafe-math-optimizationsya que se mantiene exactamente cuando no hay desbordamiento y si hay desbordamiento, obtienes un comportamiento indefinido. Entonces obtienes

foo(long):
    movq    %rdi, %rax
    imulq   %rdi, %rax
    imulq   %rdi, %rax
    imulq   %rax, %rax
    ret

con sólo -O. Para enteros sin signo, es aún más fácil ya que funcionan con potencias mod de 2 y, por lo tanto, se pueden reordenar libremente incluso ante el desbordamiento.

Charles
fuente
1
Enlace Godbolt con doble, int y sin signo. gcc y clang optimizan los tres de la misma manera (con -ffast-math)
Peter Cordes
@PeterCordes Gracias!
Charles