Estoy desarrollando algunas simulaciones de ingeniería. Esto implica implementar algunas ecuaciones largas como esta ecuación para calcular la tensión en un material similar al caucho:
T = (
mu * (
pow(l1 * pow(l1 * l2 * l3, -0.1e1 / 0.3e1), a) * a
* (
pow(l1 * l2 * l3, -0.1e1 / 0.3e1)
- l1 * l2 * l3 * pow(l1 * l2 * l3, -0.4e1 / 0.3e1) / 0.3e1
) * pow(l1 * l2 * l3, 0.1e1 / 0.3e1) / l1
- pow(l2 * pow(l1 * l2 * l3, -0.1e1 / 0.3e1), a) * a / l1 / 0.3e1
- pow(l3 * pow(l1 * l2 * l3, -0.1e1 / 0.3e1), a) * a / l1 / 0.3e1
) / a
+ K * (l1 * l2 * l3 - 0.1e1) * l2 * l3
) * N1 / l2 / l3
+ (
mu * (
- pow(l1 * pow(l1 * l2 * l3, -0.1e1 / 0.3e1), a) * a / l2 / 0.3e1
+ pow(l2 * pow(l1 * l2 * l3, -0.1e1 / 0.3e1), a) * a
* (
pow(l1 * l2 * l3, -0.1e1 / 0.3e1)
- l1 * l2 * l3 * pow(l1 * l2 * l3, -0.4e1 / 0.3e1) / 0.3e1
) * pow(l1 * l2 * l3, 0.1e1 / 0.3e1) / l2
- pow(l3 * pow(l1 * l2 * l3, -0.1e1 / 0.3e1), a) * a / l2 / 0.3e1
) / a
+ K * (l1 * l2 * l3 - 0.1e1) * l1 * l3
) * N2 / l1 / l3
+ (
mu * (
- pow(l1 * pow(l1 * l2 * l3, -0.1e1 / 0.3e1), a) * a / l3 / 0.3e1
- pow(l2 * pow(l1 * l2 * l3, -0.1e1 / 0.3e1), a) * a / l3 / 0.3e1
+ pow(l3 * pow(l1 * l2 * l3, -0.1e1 / 0.3e1), a) * a
* (
pow(l1 * l2 * l3, -0.1e1 / 0.3e1)
- l1 * l2 * l3 * pow(l1 * l2 * l3, -0.4e1 / 0.3e1) / 0.3e1
) * pow(l1 * l2 * l3, 0.1e1 / 0.3e1) / l3
) / a
+ K * (l1 * l2 * l3 - 0.1e1) * l1 * l2
) * N3 / l1 / l2;
Utilizo Maple para generar el código C ++ para evitar errores (y ahorrar tiempo con el álgebra tediosa). Como este código se ejecuta miles (si no millones) de veces, el rendimiento es una preocupación. Desafortunadamente, las matemáticas solo se simplifican hasta ahora; las ecuaciones largas son inevitables.
¿Qué enfoque puedo tomar para optimizar esta implementación? Estoy buscando estrategias de alto nivel que debería aplicar al implementar tales ecuaciones, no necesariamente optimizaciones específicas para el ejemplo que se muestra arriba.
Estoy compilando usando g ++ con --enable-optimize=-O3
.
Actualizar:
Sé que hay muchas expresiones repetidas, estoy asumiendo que el compilador las manejaría; mis pruebas hasta ahora sugieren que sí.
l1, l2, l3, mu, a, K
son todos números reales positivos (no cero).
He reemplazado l1*l2*l3
con una variable equivalente: J
. Esto ayudó a mejorar el rendimiento.
Reemplazarlo pow(x, 0.1e1/0.3e1)
con cbrt(x)
fue una buena sugerencia.
Esto se ejecutará en CPU. En un futuro cercano, probablemente funcionará mejor en GPU, pero por ahora esa opción no está disponible.
pow(l1 * l2 * l3, -0.1e1 / 0.3e1)
con una variable ... Sin embargo, debe comparar su código para asegurarse de si se ejecuta rápido o lento.Respuestas:
Editar resumen
pow(x, 0.1e1/0.3e1)
es lo mismo quecbrt(x)
.taché) esas ediciones y las empujé al final de la revisión actual de esta respuesta. Sin embargo, no los eliminé. Soy humano. Es fácil para nosotros cometer un error.l1
,l2
yl3
son números reales positivos y sia
es un número real distinto de cero. (Aún no hemos escuchado del OP sobre la naturaleza específica de estos coeficientes. Dada la naturaleza del problema, estos son supuestos razonables).Lo primero es lo primero
Maple y Mathematica a veces pasan por alto lo obvio. Aún más importante, los usuarios de Maple y Mathematica a veces cometen errores. Sustituir "a menudo", o tal vez incluso "casi siempre", en lugar de "a veces es probablemente más cercano a la marca".
Podrías haber ayudado a Maple a simplificar esa expresión contándole sobre los parámetros en cuestión. En el ejemplo que nos ocupa, sospecho que
l1
,l2
yl3
son números reales positivos y esea
es un número real distinto de cero. Si ese es el caso, dígaselo. Esos programas de matemáticas simbólicas generalmente asumen que las cantidades disponibles son complejas. La restricción del dominio permite que el programa haga suposiciones que no son válidas en los números complejos.Cómo simplificar esos grandes líos de los programas de matemáticas simbólicas (esta edición)
Los programas de matemáticas simbólicas generalmente brindan la capacidad de proporcionar información sobre los diversos parámetros. Use esa habilidad, particularmente si su problema involucra división o exponenciación. En el ejemplo que nos ocupa, se podría haber ayudado a simplificar arce esa expresión por diciéndole que
l1
,l2
yl3
son números reales positivos y quea
es un número real distinto de cero. Si ese es el caso, dígaselo. Esos programas de matemáticas simbólicas generalmente asumen que las cantidades disponibles son complejas. Restringir el dominio permite que el programa haga suposiciones como a x b x = (ab) x . Esto es solo sia
yb
son números reales positivos y six
es real. No es válido en números complejos.En última instancia, esos programas matemáticos simbólicos siguen algoritmos. Ayúdalo. Pruebe a expandir, recopilar y simplificar antes de generar código. En este caso, podría haber recopilado los términos que implican un factor de
mu
y los que implican un factor deK
. Reducir una expresión a su "forma más simple" sigue siendo un arte.Cuando tenga un feo lío de código generado, no lo acepte como una verdad que no debe tocar. Intente simplificarlo usted mismo. Mire lo que tenía el programa matemático simbólico antes de generar código. Mira cómo reduje tu expresión a algo mucho más simple y mucho más rápido, y cómo la respuesta de Walter llevó la mía varios pasos más allá. No existe una receta mágica. Si hubiera una receta mágica, Maple la habría aplicado y dado la respuesta que dio Walter.
Sobre la pregunta específica
Estás sumando y restando mucho en ese cálculo. Puede meterse en serios problemas si tiene términos que casi se cancelan entre sí. Está desperdiciando mucha CPU si tiene un término que domina sobre los demás.
A continuación, está desperdiciando mucha CPU al realizar cálculos repetidos. A menos que haya habilitado
-ffast-math
, lo que permite que el compilador rompa algunas de las reglas del punto flotante IEEE, el compilador no simplificará (de hecho, no debe) esa expresión para usted. En cambio, hará exactamente lo que le dijo que hiciera. Como mínimo, debe calcularl1 * l2 * l3
antes de calcular ese desorden.Finalmente, está haciendo muchas llamadas a
pow
, lo cual es extremadamente lento. Tenga en cuenta que varias de esas llamadas tienen el formato (l1 * l2 * l3) (1/3) . Muchas de esas llamadas apow
podrían realizarse con una sola llamada astd::cbrt
:Con este,
X * pow(l1 * l2 * l3, 0.1e1 / 0.3e1)
se convierteX * l123_pow_1_3
.X * pow(l1 * l2 * l3, -0.1e1 / 0.3e1)
se convierteX / l123_pow_1_3
.X * pow(l1 * l2 * l3, 0.4e1 / 0.3e1)
se convierteX * l123_pow_4_3
.X * pow(l1 * l2 * l3, -0.4e1 / 0.3e1)
se convierteX / l123_pow_4_3
.Maple se perdió lo obvio.
Por ejemplo, hay una forma mucho más sencilla de escribir
Suponiendo que
l1
,l2
yl3
son reales en lugar de los números complejos, y que la raíz cúbica real (en lugar de al principio raíz compleja) se van a extraer, lo anterior se reduce a:o
Usando en
cbrt_l123
lugar del123_pow_1_3
, la expresión desagradable en la pregunta se reduce aSiempre verifique dos veces, pero siempre simplifique también.
Estos son algunos de mis pasos para llegar a lo anterior:
Respuesta incorrecta, guardada intencionalmente por humildad
Tenga en cuenta que esto está afectado. Está incorrecto.
ActualizarMaple se perdió lo obvio. Por ejemplo, hay una forma mucho más sencilla de escribir
Suponiendo que
l1
,l2
yl3
son números reales en lugar de complejos, y que se debe extraer la raíz cúbica real (en lugar de la raíz compleja principal), lo anterior se reduce a cero. Este cálculo de cero se repite muchas veces.Segunda actualización
Si hice bien las matemáticas (no hay garantía de que haya hecho bien las matemáticas), la expresión desagradable en la pregunta se reduce a
Lo anterior asume quel1
,l2
yl3
son números reales positivos.fuente
-ffast-math
con gcc o clang), el compilador no puede confiar enpow(x,-1.0/3.0)
ser igual ax*pow(x,-4.0/3.0)
. Este último podría desbordar mientras que el primero no. Para cumplir con el estándar de coma flotante, el compilador no debe optimizar ese cálculo a cero.-fno-math-errno
idénticas de g ++ a CSEpow
. (A menos que tal vez pueda probar que Pow no necesitará establecer errno?)N1
,N2
yN3
no sean negativos, uno de2*N_i-(N_j+N_k)
ellos será negativo, uno será positivo y el otro estará en algún punto intermedio. Esto puede resultar fácilmente en problemas de cancelación numérica.Lo primero que hay que tener en cuenta es que
pow
es realmente caro, por lo que debe deshacerse de esto tanto como sea posible. Examinando la expresión veo muchas repeticiones depow(l1 * l2 * l3, -0.1e1 / 0.3e1)
ypow(l1 * l2 * l3, -0.4e1 / 0.3e1)
. Entonces, esperaría una gran ganancia al precalcular esos:donde estoy usando la función boost pow .
Además, tienes más
pow
con exponentea
. Sia
es Integer y se conoce en el momento del compilador, también puede reemplazarlos porboost::math::pow<a>(...)
para obtener un mayor rendimiento. También sugeriría reemplazar términos comoa / l1 / 0.3e1
con,a / (l1 * 0.3e1)
ya que la multiplicación es más rápida que la división.Finalmente, si usa g ++, puede usar la
-ffast-math
bandera que permite que el optimizador sea más agresivo en la transformación de ecuaciones. Lea sobre lo que realmente hace esta bandera , ya que tiene efectos secundarios.fuente
-ffast-math
lleva al código a volverse inestable o dar respuestas totalmente incorrectas. Tenemos un problema similar con los compiladores de Intel y tenemos que usar la-fp-model precise
opción; de lo contrario, el código explota o da las respuestas incorrectas. Entonces,-ffast-math
podría acelerarlo, pero recomendaría proceder con mucha cautela con esa opción, además de los efectos secundarios enumerados en su pregunta vinculada.-fno-math-errno
que g ++ puedapow
sacar llamadas idénticas fuera de un bucle. Esa es la parte menos "peligrosa" de -ffast-math, para la mayoría del código.pow
ser extremadamente lentos y terminamos usando eldlsym
truco mencionado en los comentarios para obtener aumentos considerables de rendimiento cuando en realidad podríamos hacerlo con un poco menos de precisión.pow
es una función pura, según el estándar, porque se supone que debe establecerse en algunas circunstancias. La configuración de indicadores como causa que no se establezca (violando así el estándar), pero entonces es una función pura y se puede optimizar como tal.errno
-fno-math-errno
errno
Woah, qué expresión tan increíble. Crear la expresión con Maple en realidad fue una elección subóptima aquí. El resultado es simplemente ilegible.
En teoría, el compilador debería poder hacer todo eso por usted, pero a veces no puede, por ejemplo, cuando el anidamiento de bucles se extiende sobre múltiples funciones en diferentes unidades de compilación. De todos modos, eso le dará un código mucho mejor legible, comprensible y fácil de mantener.
fuente
x
y noy
son variables de una sola letra sin sentido, son palabras completas con una definición precisa y un significado bien entendido.La respuesta de David Hammen es buena, pero aún está lejos de ser óptima. Continuemos con su última expresión (al momento de escribir esto)
que se puede optimizar aún más. En particular, podemos evitar la llamada ay
cbrt()
una de las llamadas apow()
si estamos explotando algunas identidades matemáticas. Hagamos esto de nuevo paso a paso.Tenga en cuenta que también lo he optimizado
2.0*N1
paraN1+N1
etc. A continuación, podemos hacerlo con solo dos llamadas apow()
.Dado que las llamadas a
pow()
son, con mucho, la operación más costosa aquí, vale la pena reducirlas tanto como sea posible (la siguiente operación costosa fue la llamada acbrt()
, que eliminamos).Si por casualidad
a
es entero, las llamadas apow
podrían optimizarse para llamadas acbrt
(más potencias enteras), o siathird
es medio entero, podemos usarsqrt
(más potencias enteras). Por otra parte, si por casualidadl1==l2
ol1==l3
ol2==l3
una o ambas llamadas apow
puede ser eliminado. Por lo tanto, vale la pena considerarlos como casos especiales si tales posibilidades existen de manera realista.fuente
He intentado una simplificación manual de esa fórmula, ¿me gustaría saber si guarda algo?
[AÑADIDO] He trabajado un poco más en la fórmula de las últimas tres líneas y lo he reducido a esta belleza:
Déjame mostrarte mi trabajo, paso a paso:
fuente
std::pow()
, de las cuales todavía tienes 6, 3 veces más de lo necesario. En otras palabras, su código es 3 veces más lento de lo posible.Esto puede ser un poco conciso, pero en realidad he encontrado una buena aceleración para polinomios (interpolación de funciones de energía) usando Horner Form, que básicamente se reescribe
ax^3 + bx^2 + cx + d
comod + x(c + x(b + x(a)))
. Esto evitará muchas llamadas repetidaspow()
y evitará que haga cosas tontas como llamar por separadopow(x,6)
y enpow(x,7)
lugar de simplemente hacerx*pow(x,6)
.Esto no se aplica directamente a su problema actual, pero si tiene polinomios de alto orden con potencias enteras, puede ayudar. Es posible que tenga que estar atento a los problemas de estabilidad y de rebose numéricos ya que el orden de las operaciones es importante para que (aunque en realidad creo que en general Formulario Horner ayuda para esto, ya
x^20
yx
son por lo general muchos órdenes de magnitud de diferencia).También como consejo práctico, si aún no lo ha hecho, intente simplificar primero la expresión en arce. Probablemente pueda conseguir que haga la mayor parte de la eliminación de subexpresiones habituales por usted. No sé cuánto afecta al generador de código en ese programa en particular, pero sé que en Mathematica hacer un FullSimplify antes de generar el código puede resultar en una gran diferencia.
fuente
Parece que tiene muchas operaciones repetidas.
Puede calcularlos previamente para no llamar repetidamente a la
pow
función, lo que puede ser costoso.También podrías pre-calutar
mientras usa ese término repetidamente.
fuente
-ffast-math
esté habilitado, y como se indica en un comentario de @ tpg2114, esa optimización puede crear resultados extremadamente inestables.Si tiene una tarjeta gráfica Nvidia CUDA, podría considerar descargar los cálculos a la tarjeta gráfica, que a su vez es más adecuada para cálculos computacionalmente complicados.
https://developer.nvidia.com/how-to-cuda-c-cpp
De lo contrario, es posible que desee considerar varios subprocesos para los cálculos.
fuente
Por casualidad, ¿podría proporcionar el cálculo simbólicamente? Si hay operaciones vectoriales, es posible que desee investigar utilizando blas o lapack, que en algunos casos pueden ejecutar operaciones en paralelo.
Es concebible (¿a riesgo de quedar fuera del tema?) Que pueda usar Python con numpy y / o scipy. En la medida de lo posible, sus cálculos podrían ser más legibles.
fuente
Como preguntó explícitamente acerca de las optimizaciones de alto nivel, podría valer la pena probar diferentes compiladores de C ++. Hoy en día, los compiladores son bestias de optimización muy complejas y los proveedores de CPU pueden implementar optimizaciones muy poderosas y específicas. Pero tenga en cuenta que algunos de ellos no son gratuitos (pero puede haber un programa académico gratuito).
He visto fragmentos de código que difieren en la velocidad de ejecución por un factor de 2, solo cambiando el compilador (con optimizaciones completas, por supuesto). Pero tenga en cuenta que debe verificar la identidad de la salida. Una optimización agresiva puede conducir a una salida diferente, que es algo que definitivamente desea evitar.
¡Buena suerte!
fuente