¿Cómo puedo mejorar el rendimiento a través de un enfoque de alto nivel al implementar ecuaciones largas en C ++?

92

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*l3con 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.

TylerH
fuente
32
Bueno, lo primero que me viene a la mente (a menos que el compilador lo optimice por sí mismo) es reemplazar todos esos 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.
SingerOfTheFall
6
También formatee el código para que sea más legible; puede ayudar a identificar posibilidades de mejora.
Ed Heal
26
¿Por qué todos los votos negativos y los votos para cerrar? Para aquellos de ustedes a quienes no les gusta la programación numérica o científica, miren otras preguntas. Esta es una buena pregunta que se adapta bien a este sitio. El sitio scicomp todavía es beta; la migración no es una buena opción. El sitio de revisión de código no tiene suficientes ojos de ciencia ficción. Lo que hizo el OP sucede con bastante frecuencia en la informática científica: construya un problema en un programa matemático simbólico, pídale al programa que genere código y no toque el resultado porque el código generado es un desastre.
David Hammen
6
@DavidHammen, el sitio de revisión de código no tiene suficientes ojos de ciencia ficción ; suena como un problema del huevo y la gallina, y una mentalidad que no está ayudando a CR a tener más ojos de este tipo. Lo mismo se aplica a la idea de rechazar el sitio beta de scicomp porque es beta ; si todo el mundo pensara así, el único sitio que podría crecer sería Stack Overflow.
Mathieu Guindon
13
Esta pregunta se está discutiendo en meta aquí
NathanOliver

Respuestas:

88

Editar resumen

  • Mi respuesta original simplemente señaló que el código contenía muchos cálculos replicados y que muchos de los poderes involucraban factores de 1/3. Por ejemplo, pow(x, 0.1e1/0.3e1)es lo mismo que cbrt(x).
  • Mi segunda edición fue simplemente incorrecta y la tercera extrapoló este error. Esto es lo que hace que la gente tenga miedo de cambiar los resultados similares a los de un oráculo de los programas de matemáticas simbólicos que comienzan con la letra 'M'. Eliminé (es decir, 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.
  • Mi cuarta edición desarrolló una expresión muy compacta que representa correctamente la expresión enrevesada en la pregunta SI los parámetros l1, l2y l3son números reales positivos y si aes 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).
  • Esta edición intenta responder al problema genérico de cómo simplificar estas expresiones.

Lo primero es lo primero

Utilizo Maple para generar el código C ++ para evitar errores.

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, l2y l3son números reales positivos y ese aes 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, l2y l3son números reales positivos y que aes 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 si ay bson números reales positivos y si xes 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 muy los que implican un factor de K. 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 calcular l1 * l2 * l3antes 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 a powpodrían realizarse con una sola llamada a std::cbrt:

l123 = l1 * l2 * l3;
l123_pow_1_3 = std::cbrt(l123);
l123_pow_4_3 = l123 * l123_pow_1_3;

Con este,

  • X * pow(l1 * l2 * l3, 0.1e1 / 0.3e1)se convierte X * l123_pow_1_3.
  • X * pow(l1 * l2 * l3, -0.1e1 / 0.3e1)se convierte X / l123_pow_1_3.
  • X * pow(l1 * l2 * l3, 0.4e1 / 0.3e1)se convierte X * l123_pow_4_3.
  • X * pow(l1 * l2 * l3, -0.4e1 / 0.3e1)se convierte X / l123_pow_4_3.


Maple se perdió lo obvio.
Por ejemplo, hay una forma mucho más sencilla de escribir

(pow(l1 * l2 * l3, -0.1e1 / 0.3e1) - l1 * l2 * l3 * pow(l1 * l2 * l3, -0.4e1 / 0.3e1) / 0.3e1)

Suponiendo que l1, l2y l3son 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:

2.0/(3.0 * pow(l1 * l2 * l3, 1.0/3.0))

o

2.0/(3.0 * l123_pow_1_3)

Usando en cbrt_l123lugar de l123_pow_1_3, la expresión desagradable en la pregunta se reduce a

l123 = l1 * l2 * l3; 
cbrt_l123 = cbrt(l123);
T = 
  mu/(3.0*l123)*(  pow(l1/cbrt_l123,a)*(2.0*N1-N2-N3)
                 + pow(l2/cbrt_l123,a)*(2.0*N2-N3-N1)
                 + pow(l3/cbrt_l123,a)*(2.0*N3-N1-N2))
 +K*(l123-1.0)*(N1+N2+N3);

Siempre verifique dos veces, pero siempre simplifique también.


Estos son algunos de mis pasos para llegar a lo anterior:

// Step 0: Trim all whitespace.
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;

// Step 1:
//   l1*l2*l3 -> l123
//   0.1e1 -> 1.0
//   0.4e1 -> 4.0
//   0.3e1 -> 3
l123 = l1 * l2 * l3;
T=(mu*(pow(l1*pow(l123,-1.0/3),a)*a*(pow(l123,-1.0/3)-l123*pow(l123,-4.0/3)/3)*pow(l123,1.0/3)/l1-pow(l2*pow(l123,-1.0/3),a)*a/l1/3-pow(l3*pow(l123,-1.0/3),a)*a/l1/3)/a+K*(l123-1.0)*l2*l3)*N1/l2/l3+(mu*(-pow(l1*pow(l123,-1.0/3),a)*a/l2/3+pow(l2*pow(l123,-1.0/3),a)*a*(pow(l123,-1.0/3)-l123*pow(l123,-4.0/3)/3)*pow(l123,1.0/3)/l2-pow(l3*pow(l123,-1.0/3),a)*a/l2/3)/a+K*(l123-1.0)*l1*l3)*N2/l1/l3+(mu*(-pow(l1*pow(l123,-1.0/3),a)*a/l3/3-pow(l2*pow(l123,-1.0/3),a)*a/l3/3+pow(l3*pow(l123,-1.0/3),a)*a*(pow(l123,-1.0/3)-l123*pow(l123,-4.0/3)/3)*pow(l123,1.0/3)/l3)/a+K*(l123-1.0)*l1*l2)*N3/l1/l2;

// Step 2:
//   pow(l123,1.0/3) -> cbrt_l123
//   l123*pow(l123,-4.0/3) -> pow(l123,-1.0/3)
//   (pow(l123,-1.0/3)-pow(l123,-1.0/3)/3) -> 2.0/(3.0*cbrt_l123)
//   *pow(l123,-1.0/3) -> /cbrt_l123
l123 = l1 * l2 * l3;
cbrt_l123 = cbrt(l123);
T=(mu*(pow(l1/cbrt_l123,a)*a*2.0/(3.0*cbrt_l123)*cbrt_l123/l1-pow(l2/cbrt_l123,a)*a/l1/3-pow(l3/cbrt_l123,a)*a/l1/3)/a+K*(l123-1.0)*l2*l3)*N1/l2/l3+(mu*(-pow(l1/cbrt_l123,a)*a/l2/3+pow(l2/cbrt_l123,a)*a*2.0/(3.0*cbrt_l123)*cbrt_l123/l2-pow(l3/cbrt_l123,a)*a/l2/3)/a+K*(l123-1.0)*l1*l3)*N2/l1/l3+(mu*(-pow(l1/cbrt_l123,a)*a/l3/3-pow(l2/cbrt_l123,a)*a/l3/3+pow(l3/cbrt_l123,a)*a*2.0/(3.0*cbrt_l123)*cbrt_l123/l3)/a+K*(l123-1.0)*l1*l2)*N3/l1/l2;

// Step 3:
//   Whitespace is nice.
l123 = l1 * l2 * l3;
cbrt_l123 = cbrt(l123);
T =
  (mu*( pow(l1/cbrt_l123,a)*a*2.0/(3.0*cbrt_l123)*cbrt_l123/l1
       -pow(l2/cbrt_l123,a)*a/l1/3
       -pow(l3/cbrt_l123,a)*a/l1/3)/a
   +K*(l123-1.0)*l2*l3)*N1/l2/l3
 +(mu*(-pow(l1/cbrt_l123,a)*a/l2/3
       +pow(l2/cbrt_l123,a)*a*2.0/(3.0*cbrt_l123)*cbrt_l123/l2
       -pow(l3/cbrt_l123,a)*a/l2/3)/a
   +K*(l123-1.0)*l1*l3)*N2/l1/l3
 +(mu*(-pow(l1/cbrt_l123,a)*a/l3/3
       -pow(l2/cbrt_l123,a)*a/l3/3
       +pow(l3/cbrt_l123,a)*a*2.0/(3.0*cbrt_l123)*cbrt_l123/l3)/a
   +K*(l123-1.0)*l1*l2)*N3/l1/l2;

// Step 4:
//   Eliminate the 'a' in (term1*a + term2*a + term3*a)/a
//   Expand (mu_term + K_term)*something to mu_term*something + K_term*something
l123 = l1 * l2 * l3;
cbrt_l123 = cbrt(l123);
T =
  (mu*( pow(l1/cbrt_l123,a)*2.0/(3.0*cbrt_l123)*cbrt_l123/l1
       -pow(l2/cbrt_l123,a)/l1/3
       -pow(l3/cbrt_l123,a)/l1/3))*N1/l2/l3
 +K*(l123-1.0)*l2*l3*N1/l2/l3
 +(mu*(-pow(l1/cbrt_l123,a)/l2/3
       +pow(l2/cbrt_l123,a)*2.0/(3.0*cbrt_l123)*cbrt_l123/l2
       -pow(l3/cbrt_l123,a)/l2/3))*N2/l1/l3
 +K*(l123-1.0)*l1*l3*N2/l1/l3
 +(mu*(-pow(l1/cbrt_l123,a)/l3/3
       -pow(l2/cbrt_l123,a)/l3/3
       +pow(l3/cbrt_l123,a)*2.0/(3.0*cbrt_l123)*cbrt_l123/l3))*N3/l1/l2
 +K*(l123-1.0)*l1*l2*N3/l1/l2;

// Step 5:
//   Rearrange
//   Reduce l2*l3*N1/l2/l3 to N1 (and similar)
//   Reduce 2.0/(3.0*cbrt_l123)*cbrt_l123/l1 to 2.0/3.0/l1 (and similar)
l123 = l1 * l2 * l3;
cbrt_l123 = cbrt(l123);
T =
  (mu*( pow(l1/cbrt_l123,a)*2.0/3.0/l1
       -pow(l2/cbrt_l123,a)/l1/3
       -pow(l3/cbrt_l123,a)/l1/3))*N1/l2/l3
 +(mu*(-pow(l1/cbrt_l123,a)/l2/3
       +pow(l2/cbrt_l123,a)*2.0/3.0/l2
       -pow(l3/cbrt_l123,a)/l2/3))*N2/l1/l3
 +(mu*(-pow(l1/cbrt_l123,a)/l3/3
       -pow(l2/cbrt_l123,a)/l3/3
       +pow(l3/cbrt_l123,a)*2.0/3.0/l3))*N3/l1/l2
 +K*(l123-1.0)*N1
 +K*(l123-1.0)*N2
 +K*(l123-1.0)*N3;

// Step 6:
//   Factor out mu and K*(l123-1.0)
l123 = l1 * l2 * l3;
cbrt_l123 = cbrt(l123);
T =
  mu*(  ( pow(l1/cbrt_l123,a)*2.0/3.0/l1
         -pow(l2/cbrt_l123,a)/l1/3
         -pow(l3/cbrt_l123,a)/l1/3)*N1/l2/l3
      + (-pow(l1/cbrt_l123,a)/l2/3
         +pow(l2/cbrt_l123,a)*2.0/3.0/l2
         -pow(l3/cbrt_l123,a)/l2/3)*N2/l1/l3
      + (-pow(l1/cbrt_l123,a)/l3/3
         -pow(l2/cbrt_l123,a)/l3/3
         +pow(l3/cbrt_l123,a)*2.0/3.0/l3)*N3/l1/l2)
 +K*(l123-1.0)*(N1+N2+N3);

// Step 7:
//   Expand
l123 = l1 * l2 * l3;
cbrt_l123 = cbrt(l123);
T =
  mu*( pow(l1/cbrt_l123,a)*2.0/3.0/l1*N1/l2/l3
      -pow(l2/cbrt_l123,a)/l1/3*N1/l2/l3
      -pow(l3/cbrt_l123,a)/l1/3*N1/l2/l3
      -pow(l1/cbrt_l123,a)/l2/3*N2/l1/l3
      +pow(l2/cbrt_l123,a)*2.0/3.0/l2*N2/l1/l3
      -pow(l3/cbrt_l123,a)/l2/3*N2/l1/l3
      -pow(l1/cbrt_l123,a)/l3/3*N3/l1/l2
      -pow(l2/cbrt_l123,a)/l3/3*N3/l1/l2
      +pow(l3/cbrt_l123,a)*2.0/3.0/l3*N3/l1/l2)
 +K*(l123-1.0)*(N1+N2+N3);

// Step 8:
//   Simplify.
l123 = l1 * l2 * l3;
cbrt_l123 = cbrt(l123);
T =
  mu/(3.0*l123)*(  pow(l1/cbrt_l123,a)*(2.0*N1-N2-N3)
                 + pow(l2/cbrt_l123,a)*(2.0*N2-N3-N1)
                 + pow(l3/cbrt_l123,a)*(2.0*N3-N1-N2))
 +K*(l123-1.0)*(N1+N2+N3);


Respuesta incorrecta, guardada intencionalmente por humildad

Tenga en cuenta que esto está afectado. Está incorrecto.

Actualizar

Maple se perdió lo obvio. Por ejemplo, hay una forma mucho más sencilla de escribir

(pow (l1 * l2 * l3, -0.1e1 / 0.3e1) - l1 * l2 * l3 * pow (l1 * l2 * l3, -0.4e1 / 0.3e1) / 0.3e1)

Suponiendo que l1, l2y l3son 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

l123 = l1 * l2 * l3; 
cbrt_l123_inv = 1.0 / cbrt(l123);
nasty_expression =
    K * (l123 - 1.0) * (N1 + N2 + N3) 
    - (  pow(l1 * cbrt_l123_inv, a) * (N2 + N3) 
       + pow(l2 * cbrt_l123_inv, a) * (N1 + N3) 
       + pow(l3 * cbrt_l123_inv, a) * (N1 + N2)) * mu / (3.0*l123);

Lo anterior asume que l1, l2y l3son números reales positivos.

David Hammen
fuente
2
Bueno, la eliminación de CSE debería funcionar, independientemente de la semántica relajada (y OP indicado en los comentarios que lo hace). Aunque, por supuesto, si importa (medido), debe inspeccionarse (ensamblaje generado). Sus puntos sobre términos dominantes, simplificaciones de fórmulas perdidas, mejores funciones especializadas y los peligros de la cancelación son muy buenos.
Deduplicador
3
@Deduplicator: no con punto flotante. A menos que uno habilite optimizaciones matemáticas inseguras (por ejemplo, especificando -ffast-mathcon gcc o clang), el compilador no puede confiar en pow(x,-1.0/3.0)ser igual a x*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.
David Hammen
Bueno, esos son mucho más ambiciosos de lo que quise decir.
Deduplicador
1
@Deduplicator: Como comenté en otra respuesta : necesitas llamadas -fno-math-errnoidénticas de g ++ a CSE pow. (A menos que tal vez pueda probar que Pow no necesitará establecer errno?)
Peter Cordes
1
@Lefti - Acepta mucho la respuesta de Walter. Es mucho más rápido. Existe un problema potencial con todas estas respuestas, que es la cancelación numérica. Suponiendo que su N1, N2y N3no sean negativos, uno de 2*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.
David Hammen
32

Lo primero que hay que tener en cuenta es que powes realmente caro, por lo que debe deshacerse de esto tanto como sea posible. Examinando la expresión veo muchas repeticiones de pow(l1 * l2 * l3, -0.1e1 / 0.3e1)y pow(l1 * l2 * l3, -0.4e1 / 0.3e1). Entonces, esperaría una gran ganancia al precalcular esos:

 const double c1 = pow(l1 * l2 * l3, -0.1e1 / 0.3e1);
const double c2 = boost::math::pow<4>(c1);

donde estoy usando la función boost pow .

Además, tienes más powcon exponente a. Si aes Integer y se conoce en el momento del compilador, también puede reemplazarlos por boost::math::pow<a>(...)para obtener un mayor rendimiento. También sugeriría reemplazar términos como a / l1 / 0.3e1con, 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-mathbandera 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.

mariomulansky
fuente
5
En nuestro código, el uso -ffast-mathlleva 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 preciseopción; de lo contrario, el código explota o da las respuestas incorrectas. Entonces, -ffast-mathpodría acelerarlo, pero recomendaría proceder con mucha cautela con esa opción, además de los efectos secundarios enumerados en su pregunta vinculada.
tpg2114
2
@ tpg2114: De acuerdo con mis pruebas, solo necesitas-fno-math-errno que g ++ pueda powsacar llamadas idénticas fuera de un bucle. Esa es la parte menos "peligrosa" de -ffast-math, para la mayoría del código.
Peter Cordes
1
@PeterCordes ¡Esos son resultados interesantes! También hemos tenido problemas con pow ser extremadamente lentos y terminamos usando el dlsymtruco mencionado en los comentarios para obtener aumentos considerables de rendimiento cuando en realidad podríamos hacerlo con un poco menos de precisión.
tpg2114
¿No entendería GCC que pow es una función pura? Probablemente sea conocimiento incorporado.
usr
6
@usr: Ese es el punto, creo. nopow 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-errnoerrno
Nate Eldredge
20

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.

  1. eligió nombres de variable hablados (no l1, l2, l3, sino por ejemplo altura, ancho, profundidad, si eso es lo que significan). Entonces le resultará más fácil comprender su propio código.
  2. calcule subterráneos, que usa varias veces, por adelantado y almacene los resultados en variables con nombres hablados.
  3. Mencionas que la expresión se evalúa muchas veces. Supongo que solo algunos parámetros varían en el bucle más interno. Calcule todos los subterráneos invariantes antes de ese ciclo. Repita para el segundo bucle interno y así sucesivamente hasta que todos los invariantes estén fuera del bucle.

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.

cdonat
fuente
8
"el compilador debería hacerlo, pero a veces no lo hace", es la clave aquí. además de la legibilidad, por supuesto.
Javier
3
Si no se requiere que el compilador haga algo, entonces asumir que eso es casi siempre incorrecto.
edmz
4
Re Elija nombres de variables hablados : muchas veces esa buena regla no se aplica cuando está haciendo matemáticas. Al mirar el código que se supone que implementa un algoritmo en una revista científica, prefiero ver que los símbolos en el código sean exactamente los que se usan en la revista. Normalmente, eso significa nombres extremadamente cortos, posiblemente con un subíndice.
David Hammen
8
"El resultado es simplemente ilegible", ¿por qué es un problema? No le importaría que la salida de lenguaje de alto nivel de un generador de lexer o analizador sea "ilegible" (por humanos). Lo que importa aquí es que la entrada al generador de código (Maple) sea legible y verificable. Lo que no debe hacer es editar el código generado a mano, si quiere estar seguro de que no tiene errores.
alephzero
3
@DavidHammen: Bueno, en ese caso, los de una letra son los "nombres que hablan". Por ejemplo, cuando se hace geometría en un sistema de coordenadas cartesiano bidimensional, xy noy son variables de una sola letra sin sentido, son palabras completas con una definición precisa y un significado bien entendido.
Jörg W Mittag
17

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)

auto l123 = l1 * l2 * l3;
auto cbrt_l123 = cbrt(l123);
T = mu/(3.0*l123)*(  pow(l1/cbrt_l123,a)*(2.0*N1-N2-N3)
                   + pow(l2/cbrt_l123,a)*(2.0*N2-N3-N1)
                   + pow(l3/cbrt_l123,a)*(2.0*N3-N1-N2))
  + K*(l123-1.0)*(N1+N2+N3);

que se puede optimizar aún más. En particular, podemos evitar la llamada ay cbrt()una de las llamadas a pow()si estamos explotando algunas identidades matemáticas. Hagamos esto de nuevo paso a paso.

// step 1 eliminate cbrt() by taking the exponent into pow()
auto l123 = l1 * l2 * l3;
auto athird = 0.33333333333333333 * a; // avoid division
T = mu/(3.0*l123)*(  (N1+N1-N2-N3)*pow(l1*l1/(l2*l3),athird)
                   + (N2+N2-N3-N1)*pow(l2*l2/(l1*l3),athird)
                   + (N3+N3-N1-N2)*pow(l3*l3/(l1*l2),athird))
  + K*(l123-1.0)*(N1+N2+N3);

Tenga en cuenta que también lo he optimizado 2.0*N1para N1+N1etc. A continuación, podemos hacerlo con solo dos llamadas a pow().

// step 2  eliminate one call to pow
auto l123 = l1 * l2 * l3;
auto athird = 0.33333333333333333 * a;
auto pow_l1l2_athird = pow(l1/l2,athird);
auto pow_l1l3_athird = pow(l1/l3,athird);
auto pow_l2l3_athird = pow_l1l3_athird/pow_l1l2_athird;
T = mu/(3.0*l123)*(  (N1+N1-N2-N3)* pow_l1l2_athird*pow_l1l3_athird
                   + (N2+N2-N3-N1)* pow_l2l3_athird/pow_l1l2_athird
                   + (N3+N3-N1-N2)/(pow_l1l3_athird*pow_l2l3_athird))
  + K*(l123-1.0)*(N1+N2+N3);

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 a cbrt(), que eliminamos).

Si por casualidad aes entero, las llamadas a powpodrían optimizarse para llamadas a cbrt(más potencias enteras), o si athirdes medio entero, podemos usar sqrt(más potencias enteras). Por otra parte, si por casualidad l1==l2o l1==l3o l2==l3una o ambas llamadas a powpuede ser eliminado. Por lo tanto, vale la pena considerarlos como casos especiales si tales posibilidades existen de manera realista.

Walter
fuente
@gnat Agradezco tu edición (pensé en hacer eso yo mismo), pero lo habría encontrado más justo si la respuesta de David también se vincule a esta. ¿Por qué no editas también la respuesta de David de manera similar?
Walter
1
Solo edité porque te vi mencionándolo explícitamente; Releí la respuesta de David y no pude encontrar una referencia a tu respuesta allí. Trato de evitar ediciones donde no está 100% claro que las cosas que agrego coinciden con las intenciones del autor
gnat
1
@Walter - Mi respuesta ahora enlaza con la tuya.
David Hammen
1
Ciertamente no fui yo. Voté tu respuesta hace unos días. También recibí un voto negativo sobre mi respuesta al azar. Las cosas simplemente pasan a veces.
David Hammen
1
Tú y yo hemos recibido un miserable voto negativo cada uno. ¡Mira todos los votos negativos sobre la pregunta! Hasta ahora, la pregunta ha recibido 16 votos negativos. También ha recibido 80 votos a favor que compensan con creces todos esos votantes en contra.
David Hammen
12
  1. ¿Cuántos son "muchos muchos"?
  2. ¿Cuánto tiempo se tarda?
  3. ¿ TODOS los parámetros cambian entre los nuevos cálculos de esta fórmula? ¿O puede almacenar en caché algunos valores precalculados?
  4. He intentado una simplificación manual de esa fórmula, ¿me gustaría saber si guarda algo?

    C1 = -0.1e1 / 0.3e1;
    C2 =  0.1e1 / 0.3e1;
    C3 = -0.4e1 / 0.3e1;
    
    X0 = l1 * l2 * l3;
    X1 = pow(X0, C1);
    X2 = pow(X0, C2);
    X3 = pow(X0, C3);
    X4 = pow(l1 * X1, a);
    X5 = pow(l2 * X1, a);
    X6 = pow(l3 * X1, a);
    X7 = a / 0.3e1;
    X8 = X3 / 0.3e1;
    X9 = mu / a;
    XA = X0 - 0.1e1;
    XB = K * XA;
    XC = X1 - X0 * X8;
    XD = a * XC * X2;
    
    XE = X4 * X7;
    XF = X5 * X7;
    XG = X6 * X7;
    
    T = (X9 * ( X4 * XD - XF - XG) / l1 + XB * l2 * l3) * N1 / l2 / l3 
      + (X9 * (-XE + X5 * XD - XG) / l2 + XB * l1 * l3) * N2 / l1 / l3 
      + (X9 * (-XE - XF + X6 * XD) / l3 + XB * l1 * l2) * N3 / l1 / l2;

[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:

T = X9 / X0 * (
      (X4 * XD - XF - XG) * N1 + 
      (X5 * XD - XE - XG) * N2 + 
      (X5 * XD - XE - XF) * N3)
  + XB * (N1 + N2 + N3)

Déjame mostrarte mi trabajo, paso a paso:

T = (X9 * (X4 * XD - XF - XG) / l1 + XB * l2 * l3) * N1 / l2 / l3 
  + (X9 * (X5 * XD - XE - XG) / l2 + XB * l1 * l3) * N2 / l1 / l3 
  + (X9 * (X5 * XD - XE - XF) / l3 + XB * l1 * l2) * N3 / l1 / l2;


T = (X9 * (X4 * XD - XF - XG) / l1 + XB * l2 * l3) * N1 / (l2 * l3) 
  + (X9 * (X5 * XD - XE - XG) / l2 + XB * l1 * l3) * N2 / (l1 * l3) 
  + (X9 * (X5 * XD - XE - XF) / l3 + XB * l1 * l2) * N3 / (l1 * l2);

T = (X9 * (X4 * XD - XF - XG) + XB * l1 * l2 * l3) * N1 / (l1 * l2 * l3) 
  + (X9 * (X5 * XD - XE - XG) + XB * l1 * l2 * l3) * N2 / (l1 * l2 * l3) 
  + (X9 * (X5 * XD - XE - XF) + XB * l1 * l2 * l3) * N3 / (l1 * l2 * l3);

T = (X9 * (X4 * XD - XF - XG) + XB * X0) * N1 / X0 
  + (X9 * (X5 * XD - XE - XG) + XB * X0) * N2 / X0 
  + (X9 * (X5 * XD - XE - XF) + XB * X0) * N3 / X0;

T = X9 * (X4 * XD - XF - XG) * N1 / X0 + XB * N1 
  + X9 * (X5 * XD - XE - XG) * N2 / X0 + XB * N2
  + X9 * (X5 * XD - XE - XF) * N3 / X0 + XB * N3;


T = X9 * (X4 * XD - XF - XG) * N1 / X0 
  + X9 * (X5 * XD - XE - XG) * N2 / X0
  + X9 * (X5 * XD - XE - XF) * N3 / X0
  + XB * (N1 + N2 + N3)
Vlad Feinstein
fuente
2
Eso es notable, ¿eh? :) FORTRAN, IIRC, fue diseñado para cálculos de fórmulas eficientes ("FOR" es para fórmula).
Vlad Feinstein
La mayoría de los códigos F77 que he visto se veían así (por ejemplo, BLAS y NR). Muy contento de que Fortran 90-> 2008 exista :)
Kyle Kanos
Si. Si está traduciendo una fórmula, ¿qué mejor manera que FORmulaTRANslation?
Brian Drummond
1
Su 'optimización' ataca el lugar equivocado. Los bits costosos son las llamadas a 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.
Walter
7

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 + dcomo d + x(c + x(b + x(a))). Esto evitará muchas llamadas repetidas pow()y evitará que haga cosas tontas como llamar por separado pow(x,6)y en pow(x,7)lugar de simplemente hacer x*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^20y xson 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.

neocpp
fuente
La forma Horner es bastante estándar para codificar polinomios y esto no tiene ninguna relevancia para la pregunta.
Walter
1
Esto puede ser cierto dado su ejemplo, pero notará que dijo "ecuaciones de este tipo". Pensé que la respuesta sería útil si el cartel tuviera polinomios en su sistema. Particularmente he notado que los generadores de código para los programas CAS como Mathematica y Maple tienden a NO darle forma Horner a menos que la solicite específicamente; por defecto son la forma en que normalmente escribirías un polinomio como humano.
neocpp
3

Parece que tiene muchas operaciones repetidas.

pow(l1 * l2 * l3, -0.1e1 / 0.3e1)
pow(l1 * l2 * l3, -0.4e1 / 0.3e1)

Puede calcularlos previamente para no llamar repetidamente a la powfunción, lo que puede ser costoso.

También podrías pre-calutar

l1 * l2 * l3

mientras usa ese término repetidamente.

NathanOliver
fuente
6
Apuesto a que el optimizador ya hace esto por usted ... aunque hace que al menos el código sea más legible.
Karoly Horvath
Hice esto, pero no aceleró las cosas en absoluto. Supuse que esto se debía a que la optimización del compilador ya se estaba ocupando de ello.
Sin embargo, almacenar l1 * l2 * l3 acelera las cosas, no estoy seguro de por qué con la optimización del compilador
porque el compilador a veces simplemente no puede hacer algunas optimizaciones o las encuentra en conflicto con otras opciones.
Javier
1
De hecho, el compilador no debe realizar esas optimizaciones a menos que -ffast-mathesté habilitado, y como se indica en un comentario de @ tpg2114, esa optimización puede crear resultados extremadamente inestables.
David Hammen
0

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.

usuario3791372
fuente
10
Esta respuesta es ortogonal a la pregunta que nos ocupa. Si bien las GPU tienen muchos procesadores, son bastante lentas en comparación con la FPU integrada en la CPU. Realizar un solo cálculo en serie con una GPU es una gran pérdida. La CPU tiene que llenar la canalización hacia la GPU, esperar a que la GPU lenta realice esa única tarea y luego descargar el resultado. Si bien las GPU son absolutamente fantásticas cuando el problema en cuestión se puede paralelizar masivamente, son absolutamente atroces cuando se trata de realizar tareas en serie.
David Hammen
1
En la pregunta original: "Como este código se ejecuta muchas veces, el rendimiento es una preocupación". Eso es uno más que "muchos". El operador puede enviar los cálculos de manera secuenciada.
user3791372
0

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.

Fred Mitchell
fuente
0

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).

  • La colección de compiladores GNU es gratuita, flexible y está disponible en muchas arquitecturas
  • Los compiladores de Intel son muy rápidos, muy costosos y también pueden producir buenos resultados para las arquitecturas AMD (creo que hay un programa académico)
  • Los compiladores de Clang son rápidos, gratuitos y pueden producir resultados similares a GCC (algunas personas dicen que son más rápidos, mejores, pero esto puede diferir para cada caso de aplicación, sugiero que cree sus propias experiencias)
  • PGI (Portland Group) no es gratuito como los compiladores de Intel.
  • Los compiladores PathScale pueden tener buenos resultados en arquitecturas AMD

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!

matemáticas
fuente