c++ math floating-point

c++ - ¿Hay alguna ventaja de usar pow(x, 2) en lugar de x*x, con x double?



math floating-point (8)

¿hay alguna ventaja al usar este código?

double x; double square = pow(x,2);

¿en lugar de esto?

double x; double square = x*x;

Prefiero x * x y mirando mi implementación (Microsoft). No encuentro ventajas en pow porque x * x es más simple que pow para el caso cuadrado particular.

¿Hay algún caso particular en que el pow sea superior?


EN MI HUMILDE OPINIÓN:

  • Legibilidad del código
  • Fortaleza del código: será más fácil cambiar a pow(x, 6) , quizás se implemente algún mecanismo de coma flotante para un procesador específico, etc.
  • Rendimiento: si hay una manera más inteligente y más rápida de calcular esto (utilizando ensamblador o algún tipo de truco especial), pow lo hará. no lo harás ... :)

Aclamaciones


En C ++ 11 hay un caso en el que hay una ventaja al usar x * x sobre std::pow(x,2) y ese es el caso donde necesita usarlo en un constexpr :

constexpr double mySqr( double x ) { return x * x ; }

Como podemos ver, std::pow no está marcado como constexpr, por lo que no se puede usar en una función constexpr .

De lo contrario, desde una perspectiva de rendimiento poner el siguiente código en godbolt muestra estas funciones:

#include <cmath> double mySqr( double x ) { return x * x ; } double mySqr2( double x ) { return std::pow( x, 2.0 ); }

generar ensamblaje idéntico:

mySqr(double): mulsd %xmm0, %xmm0 # x, D.4289 ret mySqr2(double): mulsd %xmm0, %xmm0 # x, D.4292 ret

y deberíamos esperar resultados similares de cualquier compilador moderno.

Vale la pena señalar que actualmente gcc considera a pow como un constento , también cubierto here pero esta es una extensión no conforme y no debe confiarse en ella y probablemente cambie en versiones posteriores de gcc .


Esta pregunta toca una de las debilidades clave de la mayoría de las implementaciones de C y C ++ con respecto a la programación científica. Después de pasar de Fortran a C unos veinte años, y más tarde a C ++, este sigue siendo uno de esos puntos dolorosos que ocasionalmente me preguntan si ese cambio fue algo bueno.

El problema en pocas palabras:

  • La forma más fácil de implementar pow es Type pow(Type x; Type y) {return exp(y*log(x));}
  • La mayoría de los compiladores C y C ++ toman el camino más fácil.
  • Algunos podrían ''hacer lo correcto'', pero solo a altos niveles de optimización.
  • Comparado con x*x , la salida fácil con pow(x,2) es extremadamente costosa computacionalmente y pierde precisión.

Compare con los lenguajes dirigidos a la programación científica:

  • Usted no escribe pow(x,y) . Estos lenguajes tienen un operador de exponenciación incorporado. Que C y C ++ se hayan negado rotundamente a implementar un operador de exponenciación hace hervir la sangre de muchos programadores de programadores científicos. Para algunos acérrimos programadores de Fortran, esto solo es motivo para nunca cambiar a C.
  • Se requiere que Fortran (y otros lenguajes) ''hagan lo correcto'' para todas las potencias enteras pequeñas, donde lo pequeño es cualquier número entero entre -12 y 12. (El compilador no cumple si no puede ''hacer lo correcto'' .) Además, están obligados a hacerlo con la optimización desactivada.
  • Muchos compiladores de Fortran también saben cómo extraer algunas raíces racionales sin recurrir a la salida fácil.

Existe un problema al confiar en altos niveles de optimización para ''hacer lo correcto''. He trabajado para varias organizaciones que han prohibido el uso de la optimización en software de seguridad crítica. Los recuerdos pueden ser muy largos (varias décadas) luego de perder 10 millones de dólares aquí, 100 millones allí, todo debido a errores en algún compilador de optimización.

En mi humilde opinión, nunca se debe usar pow(x,2) en C o C ++. No estoy solo en esta opinión. Los programadores que sí usan pow(x,2) generalmente se vuelven demasiado grandes durante las revisiones de código.


FWIW, con gcc-4.2 en MacOS X 10.6 y -O3 indicadores del compilador,

x = x * x;

y

y = pow(y, 2);

dar como resultado el mismo código de ensamblaje:

#include <cmath> void test(double& x, double& y) { x = x * x; y = pow(y, 2); }

Se ensambla a:

pushq %rbp movq %rsp, %rbp movsd (%rdi), %xmm0 mulsd %xmm0, %xmm0 movsd %xmm0, (%rdi) movsd (%rsi), %xmm0 mulsd %xmm0, %xmm0 movsd %xmm0, (%rsi) leave ret

Así que mientras uses un compilador decente, escribe el que tenga más sentido para tu aplicación, pero considera que pow(x, 2) nunca puede ser más óptimo que la simple multiplicación.


No solo es x*x más claro, será al menos tan rápido como pow(x,2) .


Probablemente elegiría std::pow(x, 2) porque podría hacer que mi código de refactorización sea más fácil. Y no tendría ninguna importancia una vez que el código esté optimizado.

Ahora, los dos enfoques no son idénticos. Este es mi código de prueba:

#include<cmath> double square_explicit(double x) { asm("### Square Explicit"); return x * x; } double square_library(double x) { asm("### Square Library"); return std::pow(x, 2); }

El asm("text"); call simplemente escribe comentarios en el resultado del ensamblado, que produzco utilizando (GCC 4.8.1 en OS X 10.7.4):

g++ example.cpp -c -S -std=c++11 -O[0, 1, 2, or 3]

No necesita -std=c++11 , siempre lo uso.

Primero: cuando se depura (con optimización cero), el ensamblaje producido es diferente; esta es la parte relevante:

# 4 "square.cpp" 1 ### Square Explicit # 0 "" 2 movq -8(%rbp), %rax movd %rax, %xmm1 mulsd -8(%rbp), %xmm1 movd %xmm1, %rax movd %rax, %xmm0 popq %rbp LCFI2: ret LFE236: .section __TEXT,__textcoal_nt,coalesced,pure_instructions .globl __ZSt3powIdiEN9__gnu_cxx11__promote_2IT_T0_NS0_9__promoteIS2_XsrSt12__is_integerIS2_E7__valueEE6__typeENS4_IS3_XsrS5_IS3_E7__valueEE6__typeEE6__typeES2_S3_ .weak_definition __ZSt3powIdiEN9__gnu_cxx11__promote_2IT_T0_NS0_9__promoteIS2_XsrSt12__is_integerIS2_E7__valueEE6__typeENS4_IS3_XsrS5_IS3_E7__valueEE6__typeEE6__typeES2_S3_ __ZSt3powIdiEN9__gnu_cxx11__promote_2IT_T0_NS0_9__promoteIS2_XsrSt12__is_integerIS2_E7__valueEE6__typeENS4_IS3_XsrS5_IS3_E7__valueEE6__typeEE6__typeES2_S3_: LFB238: pushq %rbp LCFI3: movq %rsp, %rbp LCFI4: subq $16, %rsp movsd %xmm0, -8(%rbp) movl %edi, -12(%rbp) cvtsi2sd -12(%rbp), %xmm2 movd %xmm2, %rax movq -8(%rbp), %rdx movd %rax, %xmm1 movd %rdx, %xmm0 call _pow movd %xmm0, %rax movd %rax, %xmm0 leave LCFI5: ret LFE238: .text .globl __Z14square_libraryd __Z14square_libraryd: LFB237: pushq %rbp LCFI6: movq %rsp, %rbp LCFI7: subq $16, %rsp movsd %xmm0, -8(%rbp) # 9 "square.cpp" 1 ### Square Library # 0 "" 2 movq -8(%rbp), %rax movl $2, %edi movd %rax, %xmm0 call __ZSt3powIdiEN9__gnu_cxx11__promote_2IT_T0_NS0_9__promoteIS2_XsrSt12__is_integerIS2_E7__valueEE6__typeENS4_IS3_XsrS5_IS3_E7__valueEE6__typeEE6__typeES2_S3_ movd %xmm0, %rax movd %rax, %xmm0 leave LCFI8: ret

Pero cuando -O1 el código optimizado (incluso en el nivel de optimización más bajo para GCC, es decir, -O1 ), el código es idéntico:

# 4 "square.cpp" 1 ### Square Explicit # 0 "" 2 mulsd %xmm0, %xmm0 ret LFE236: .globl __Z14square_libraryd __Z14square_libraryd: LFB237: # 9 "square.cpp" 1 ### Square Library # 0 "" 2 mulsd %xmm0, %xmm0 ret

Entonces, realmente no hace ninguna diferencia a menos que te importe la velocidad del código no optimizado.

Como dije: me parece que std::pow(x, 2) transmite más claramente tus intenciones, pero esa es una cuestión de preferencia, no de rendimiento.

Y la optimización parece mantenerse incluso para expresiones más complejas. Tome, por ejemplo:

double explicit_harder(double x) { asm("### Explicit, harder"); return x * x - std::sin(x) * std::sin(x) / (1 - std::tan(x) * std::tan(x)); } double implicit_harder(double x) { asm("### Library, harder"); return std::pow(x, 2) - std::pow(std::sin(x), 2) / (1 - std::pow(std::tan(x), 2)); }

De nuevo, con -O1 (la optimización más baja), el ensamblaje es idéntico una vez más:

# 14 "square.cpp" 1 ### Explicit, harder # 0 "" 2 call _sin movd %xmm0, %rbp movd %rbx, %xmm0 call _tan movd %rbx, %xmm3 mulsd %xmm3, %xmm3 movd %rbp, %xmm1 mulsd %xmm1, %xmm1 mulsd %xmm0, %xmm0 movsd LC0(%rip), %xmm2 subsd %xmm0, %xmm2 divsd %xmm2, %xmm1 subsd %xmm1, %xmm3 movapd %xmm3, %xmm0 addq $8, %rsp LCFI3: popq %rbx LCFI4: popq %rbp LCFI5: ret LFE239: .globl __Z15implicit_harderd __Z15implicit_harderd: LFB240: pushq %rbp LCFI6: pushq %rbx LCFI7: subq $8, %rsp LCFI8: movd %xmm0, %rbx # 19 "square.cpp" 1 ### Library, harder # 0 "" 2 call _sin movd %xmm0, %rbp movd %rbx, %xmm0 call _tan movd %rbx, %xmm3 mulsd %xmm3, %xmm3 movd %rbp, %xmm1 mulsd %xmm1, %xmm1 mulsd %xmm0, %xmm0 movsd LC0(%rip), %xmm2 subsd %xmm0, %xmm2 divsd %xmm2, %xmm1 subsd %xmm1, %xmm3 movapd %xmm3, %xmm0 addq $8, %rsp LCFI9: popq %rbx LCFI10: popq %rbp LCFI11: ret

Finalmente: el enfoque x * x no requiere include cmath que haría que tu compilación sea ligeramente más rápida si todo lo demás cmath igual.


std :: pow es más expresivo si te refieres a x², xx es más expresivo si te refieres x x, especialmente si solo estás codificando, por ejemplo, un artículo científico y los lectores deberían ser capaces de entender tu implementación vs. el papel. La diferencia es sutil, tal vez para x * x / x², pero creo que si usas funciones con nombre en general, aumenta el sentido del código y la legibilidad.

En los compiladores modernos, como por ejemplo, g ++ 4.x, std :: pow (x, 2) estará en línea, si ni siquiera es un compilador incorporado, y la fuerza se reduce a x * x. Si no es así por defecto y no le importa la conformidad de tipo flotante IEEE, consulte el manual de su compilador para un cambio rápido de matemáticas (g ++ == -fast-math).

Sidenote: Se ha mencionado que incluir math.h aumenta el tamaño del programa. Mi respuesta fue:

En C ++, #include <cmath> , no math.h. Además, si su compilador no es antiguo, aumentará el tamaño de sus programas solo por lo que está usando (en el caso general), y si su implementación de std :: pow se alinea con las instrucciones x87 correspondientes, y un g ++ moderno fuerza-reduce x² con x * x, entonces no hay aumento de tamaño relevante. Además, el tamaño del programa nunca debe dictar qué expresivo es tu código.

Otra ventaja de cmath sobre math.h es que con cmath, obtienes una sobrecarga std :: pow para cada tipo de punto flotante, mientras que con math.h obtienes pow, powf, etc. en el espacio de nombres global, por lo que aumenta la adaptabilidad de código, especialmente al escribir plantillas.

Como regla general: prefiera el código expresivo y claro sobre el rendimiento dudosamente fundamentado y el código razonado de tamaño binario.

Ver también Knuth:

"Deberíamos olvidarnos de las pequeñas eficiencias, digamos el 97% del tiempo: la optimización prematura es la raíz de todo mal"

y Jackson:

La primera regla de la optimización del programa: no lo hagas. La segunda regla de optimización del programa (¡solo para expertos!): No lo hagas aún.


x * x siempre compilará a una simple multiplicación. Es probable que pow(x, 2) , pero de ninguna manera garantizado, se optimice al mismo. Si no está optimizado, es probable que use una rutina matemática general lenta para aumentar la potencia. Entonces, si el rendimiento es su preocupación, siempre debe favorecer a x * x .