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
esType 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 conpow(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
.