web-dev-qa-db-fra.com

Comment puis-je améliorer les performances via une approche de haut niveau lors de l'implémentation de longues équations en C ++

Je développe des simulations d'ingénierie. Cela implique la mise en œuvre de longues équations telles que cette équation pour calculer la contrainte dans un matériau de type caoutchouc:

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;

J'utilise Maple pour générer le code C++ pour éviter les erreurs (et gagner du temps avec une algèbre fastidieuse). Comme ce code est exécuté des milliers (sinon des millions) de fois, les performances sont un problème. Malheureusement, les calculs ne se simplifient que jusqu'à présent; les équations longues sont inévitables.

Quelle approche puis-je adopter pour optimiser cette implémentation? Je recherche des stratégies de haut niveau que je devrais appliquer lors de l'implémentation de telles équations, pas nécessairement des optimisations spécifiques pour l'exemple ci-dessus.

Je compile en utilisant g ++ avec --enable-optimize=-O3.

Mise à jour:

Je sais qu'il y a beaucoup d'expressions répétées, j'utilise l'hypothèse que le compilateur les gérerait; mes tests jusqu'à présent suggèrent que c'est le cas.

l1, l2, l3, mu, a, K Sont tous des nombres réels positifs (pas zéro).

J'ai remplacé l1*l2*l3 Par une variable équivalente: J. Cela a permis d'améliorer les performances.

Remplacer pow(x, 0.1e1/0.3e1) par cbrt(x) était une bonne suggestion.

Cela sera exécuté sur les processeurs, dans un avenir proche, cela fonctionnerait probablement mieux sur les GPU, mais pour l'instant cette option n'est pas disponible.

91
user602095

Modifier le résumé

  • Ma réponse originale a simplement noté que le code contenait beaucoup de calculs répliqués et que de nombreux pouvoirs impliquaient des facteurs de 1/3. Par exemple, pow(x, 0.1e1/0.3e1) est identique à cbrt(x).
  • Mon deuxième montage était tout simplement faux, et mon troisième a extrapolé sur cette erreur. C'est ce qui fait que les gens ont peur de modifier les résultats de type Oracle des programmes mathématiques symboliques qui commencent par la lettre "M". J'ai rayé (c'est-à-dire, la grève) ces modifications et les a poussés au bas de la révision actuelle de cette réponse. Cependant, je ne les ai pas supprimés. Je suis humain. Il est facile pour nous de faire une erreur.
  • Ma quatrième édition a développé une expression très compacte qui représente correctement l'expression alambiquée dans la question [~ # ~] si [~ # ~] les paramètres l1, l2 Et l3 Sont des nombres réels positifs et si a est un nombre réel non nul. (Nous n'avons pas encore entendu le PO sur la nature spécifique de ces coefficients. Compte tenu de la nature du problème, ce sont des hypothèses raisonnables.)
  • Cette modification tente de répondre au problème générique de la façon de simplifier ces expressions.

Tout d'abord

J'utilise Maple pour générer le code C++ pour éviter les erreurs.

Maple et Mathematica manquent parfois l'évidence. Plus important encore, les utilisateurs de Maple et Mathematica font parfois des erreurs. La substitution de "souvent", ou peut-être même "presque toujours", au lieu de "parfois est probablement plus proche de la marque.

Vous auriez pu aider Maple à simplifier cette expression en lui parlant des paramètres en question. Dans l'exemple présenté, je soupçonne que l1, l2 Et l3 Sont des nombres réels positifs et que a est un nombre réel non nul. Si tel est le cas, dites-le lui. Ces programmes mathématiques symboliques supposent généralement que les quantités disponibles sont complexes. Restreindre le domaine permet au programme de faire des hypothèses qui ne sont pas valides dans les nombres complexes.


Comment simplifier ces gros dégâts à partir de programmes mathématiques symboliques (cette modification)

Les programmes mathématiques symboliques permettent généralement de fournir des informations sur les différents paramètres. Utilisez cette capacité, en particulier si votre problème implique une division ou une exponentiation. Dans l'exemple présenté, vous auriez pu aider Maple à simplifier cette expression en lui disant que l1, l2 Et l3 Sont des nombres réels positifs et que a est un nombre réel non nul. Si tel est le cas, dites-le lui. Ces programmes mathématiques symboliques supposent généralement que les quantités disponibles sont complexes. La restriction du domaine permet au programme de faire des hypothèses telles qu'unxbx= (ab)x. C'est seulement si a et b sont des nombres réels positifs et si x est réel. Il n'est pas valable dans les nombres complexes.

En fin de compte, ces programmes mathématiques symboliques suivent des algorithmes. Aidez-le. Essayez de développer, de collecter et de simplifier avant de générer du code. Dans ce cas, vous auriez pu collecter les termes impliquant un facteur mu et ceux impliquant un facteur K. Réduire une expression à sa "forme la plus simple" reste un peu un art.

Lorsque vous obtenez un désordre laid de code généré, ne l'acceptez pas comme une vérité que vous ne devez pas toucher. Essayez de le simplifier vous-même. Regardez ce que le programme mathématique symbolique avait avant de générer du code. Regardez comment j'ai réduit votre expression à quelque chose de beaucoup plus simple et beaucoup plus rapide, et comment réponse de Walter a pris le mien plusieurs étapes plus loin. Il n'y a pas de recette magique. S'il y avait une recette magique, Maple l'aurait appliquée et aurait donné la réponse que Walter avait donnée.


À propos de la question spécifique

Vous faites beaucoup d'addition et de soustraction dans ce calcul. Vous pouvez avoir de gros ennuis si vous avez des conditions qui s'annulent presque. Vous gaspillez beaucoup de CPU si vous avez un terme qui domine sur les autres.

Ensuite, vous gaspillez beaucoup de CPU en effectuant des calculs répétés. À moins que vous n'ayez activé -ffast-math, Qui permet au compilateur d'enfreindre certaines des règles de la virgule flottante IEEE, le compilateur ne simplifiera pas (en fait, ne doit pas) cette expression pour vous. Il fera à la place exactement ce que vous lui avez dit de faire. Au minimum, vous devez calculer l1 * l2 * l3 Avant de calculer ce désordre.

Enfin, vous passez beaucoup d'appels à pow, ce qui est extrêmement lent. Notez que plusieurs de ces appels sont de la forme (l1 * l2 * l3)(1/3). Beaucoup de ces appels à pow pourraient être effectués avec un seul appel à std::cbrt:

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

Avec ça,

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


Maple a raté l'évidence.
Par exemple, il existe un moyen beaucoup plus simple d'écrire

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

En supposant que l1, l2 Et l3 Sont des nombres réels plutôt que complexes, et que la racine réelle du cube (plutôt que la racine complexe principale) doit être extraite, le ci-dessus se réduit à

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

ou

2.0/(3.0 * l123_pow_1_3)

En utilisant cbrt_l123 Au lieu de l123_pow_1_3, L'expression désagréable de la question se réduit à

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

Vérifiez toujours deux fois, mais simplifiez toujours aussi.


Voici certaines de mes étapes pour arriver à ce qui précède:

// 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);


Mauvaise réponse, intentionnellement gardée pour l'humilité

Notez que cela est frappé. C'est faux.

Mise à jour

Maple a raté l'évidence. Par exemple, il existe un moyen beaucoup plus simple d'écrire

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

En supposant que l1, l2 Et l3 Sont des nombres réels plutôt que complexes, et que la racine réelle du cube (plutôt que la racine complexe principale) doit être extraite, le ci-dessus se réduit à zéro. Ce calcul de zéro est répété plusieurs fois.

Deuxième mise à jour

Si j'ai bien fait les maths (il n'y a pas garantie que j'ai bien fait les maths), l'expression désagréable de la question se réduit à

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

Ce qui précède suppose que l1, l2 Et l3 Sont des nombres réels positifs.

87
David Hammen

La première chose à noter est que pow est vraiment cher, vous devez donc vous en débarrasser autant que possible. En parcourant l'expression, je vois de nombreuses répétitions de pow(l1 * l2 * l3, -0.1e1 / 0.3e1) et pow(l1 * l2 * l3, -0.4e1 / 0.3e1). Je m'attendrais donc à un gros gain en pré-calculant ceux-ci:

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

où j'utilise la fonction boost pow .

De plus, vous avez encore pow avec l'exposant a. Si a est Integer et connu au moment du compilateur, vous pouvez également remplacer ceux par boost::math::pow<a>(...) pour obtenir des performances supplémentaires. Je suggère également de remplacer des termes comme a / l1 / 0.3e1 Par a / (l1 * 0.3e1) car la multiplication est plus rapide que la division.

Enfin, si vous utilisez g ++, vous pouvez utiliser l'indicateur -ffast-math Qui permet à l'optimiseur d'être plus agressif dans la transformation des équations. Lisez sur ce que fait réellement cet indicateur , car il a cependant des effets secondaires.

31
mariomulansky

Woah, quel enfer d'expression. La création de l'expression avec Maple était en fait un choix sous-optimal ici. Le résultat est tout simplement illisible.

  1. choisir des noms de variables parlantes (pas l1, l2, l3, mais par exemple hauteur, largeur, profondeur, si c'est ce qu'ils signifient). Il vous sera alors plus facile de comprendre votre propre code.
  2. calculer des sous-termes, que vous utilisez plusieurs fois, à l'avance et stocker les résultats dans des variables avec des noms parlants.
  3. Vous mentionnez que l'expression est évaluée très souvent. Je suppose que seuls quelques paramètres varient dans la boucle la plus interne. Calculez tous les sous-termes invariants avant cette boucle. Répétez l'opération pour la deuxième boucle intérieure et ainsi de suite jusqu'à ce que tous les invariants soient en dehors de la boucle.

Théoriquement, le compilateur devrait être capable de faire tout cela pour vous, mais parfois il ne peut pas - par exemple lorsque l'imbrication de boucle se propage sur plusieurs fonctions dans différentes unités de compilation. Quoi qu'il en soit, cela vous donnera un code beaucoup plus lisible, compréhensible et maintenable.

20
cdonat

réponse de David Hammen est bon, mais encore loin d'être optimal. Continuons avec sa dernière expression (au moment d'écrire ceci)

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

qui peut être optimisé davantage. En particulier, nous pouvons éviter l'appel à cbrt() et l'un des appels à pow() si nous exploitons certaines identités mathématiques. Faisons-le à nouveau étape par étape.

// 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);

Notez que j'ai également optimisé 2.0*N1 En N1+N1 Etc. Ensuite, nous pouvons faire avec seulement deux appels à 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);

Étant donné que les appels à pow() sont de loin l'opération la plus coûteuse ici, il vaut la peine de les réduire autant que possible (la prochaine opération coûteuse a été l'appel à cbrt(), que nous avons éliminée ).

Si par hasard a est entier, les appels à pow pourraient être optimisés en appels à cbrt (plus les puissances entières), ou si athird est à moitié -entier, nous pouvons utiliser sqrt (plus les puissances entières). De plus, si par hasard l1==l2 Ou l1==l3 Ou l2==l3 Un ou les deux appels à pow peuvent être éliminés. Donc, cela vaut la peine de les considérer comme des cas spéciaux si de telles chances existent de manière réaliste.

17
Walter
  1. Combien est "plusieurs"?
  2. Combien de temps cela prend-il?
  3. [~ # ~] tous les paramètres [~ # ~] changent-ils entre le recalcul de cette formule? Ou pouvez-vous mettre en cache certaines valeurs pré-calculées?
  4. J'ai tenté une simplification manuelle de cette formule, je voudrais savoir si elle enregistre quelque chose?

    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;
    

[AJOUTÉ] J'ai encore travaillé sur la dernière formule à trois lignes et je suis parvenu à cette beauté:

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

Permettez-moi de montrer mon travail, étape par étape:

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)
12
Vlad Feinstein

Cela peut être un peu laconique, mais j'ai en fait trouvé une bonne accélération pour les polynômes (interpolation des fonctions énergétiques) en utilisant Horner Form, qui réécrit essentiellement ax^3 + bx^2 + cx + d En d + x(c + x(b + x(a))). Cela évitera de nombreux appels répétés à pow() et vous empêchera de faire des choses idiotes comme appeler séparément pow(x,6) et pow(x,7) au lieu de simplement faire x*pow(x,6).

Ce n'est pas directement applicable à votre problème actuel, mais si vous avez des polynômes d'ordre élevé avec des puissances entières, cela peut aider. Vous devrez peut-être faire attention aux problèmes de stabilité numérique et de débordement car l'ordre des opérations est important pour cela (bien qu'en général, je pense que Horner Form y contribue, car x^20 Et x sont généralement plusieurs ordres de grandeur à part).

Également comme astuce pratique, si vous ne l'avez pas déjà fait, essayez d'abord de simplifier l'expression en érable. Vous pouvez probablement le faire faire la plupart de l'élimination de la sous-expression commune pour vous. Je ne sais pas dans quelle mesure cela affecte le générateur de code dans ce programme en particulier, mais je sais que dans Mathematica, faire un FullSimplify avant de générer le code peut entraîner une énorme différence.

7
neocpp

Il semble que vous ayez beaucoup d'opérations répétées en cours.

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

Vous pouvez les pré-calculer afin de ne pas appeler à plusieurs reprises la fonction pow qui peut être coûteuse.

Vous pouvez également pré-caluter

l1 * l2 * l3

que vous utilisez ce terme à plusieurs reprises.

3
NathanOliver

Par hasard, pourriez-vous fournir le calcul symboliquement. S'il y a des opérations vectorielles, vous voudrez peut-être vraiment étudier l'utilisation de blas ou de lapack qui, dans certains cas, peut exécuter des opérations en parallèle.

Il est concevable (au risque d'être hors sujet?) Que vous puissiez utiliser python avec numpy et/ou scipy. Dans la mesure où cela était possible, vos calculs pourraient être plus lisible.

0
Fred Mitchell

Si vous avez une carte graphique Nvidia CUDA, vous pouvez envisager de décharger les calculs sur la carte graphique, qui est elle-même plus adaptée aux calculs complexes.

https://developer.nvidia.com/how-to-cuda-c-cpp

Sinon, vous voudrez peut-être considérer plusieurs threads pour les calculs.

0
user3791372

Comme vous l'avez explicitement demandé à propos des optimisations de haut niveau, il peut être utile d'essayer différents compilateurs C++. De nos jours, les compilateurs sont des bêtes d'optimisation très complexes et les fournisseurs de CPU peuvent implémenter des optimisations très puissantes et spécifiques. Mais veuillez noter que certains d'entre eux ne sont pas gratuits (mais il pourrait y avoir un programme académique gratuit).

  • La collection de compilateurs GNU est gratuite, flexible et disponible sur de nombreuses architectures
  • Les compilateurs Intel sont très rapides, très chers et peuvent également produire de bons résultats pour les architectures AMD (je crois qu'il existe un programme académique)
  • Les compilateurs Clang sont rapides, gratuits et peuvent produire des résultats similaires à GCC (certaines personnes disent qu'ils sont plus rapides, meilleurs, mais cela peut différer pour chaque cas d'application, je suggère de faire vos propres expériences)
  • PGI (Portland Group) n'est pas gratuit comme les compilateurs Intel.
  • Les compilateurs PathScale peuvent obtenir de bons résultats sur les architectures AMD

J'ai vu des extraits de code différer dans la vitesse d'exécution par le facteur 2, uniquement en changeant le compilateur (avec des optimisations complètes bien sûr). Mais n'oubliez pas de vérifier l'identité de la sortie. Une optimisation agressive peut conduire à des sorties différentes, ce que vous voulez absolument éviter.

Bonne chance!

0
math