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


92

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 (voire des millions) de fois, les performances sont un problème. Malheureusement, le calcul ne simplifie que jusqu'à présent; les longues équations 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 la mise en œuvre de telles équations, pas nécessairement des optimisations spécifiques pour l'exemple ci-dessus.

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

Mettre à 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 le suggèrent.

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

Je l' ai remplacé l1*l2*l3par une variable équivalente: J. Cela a aidé à 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 proche avenir, cela fonctionnerait probablement mieux sur les GPU, mais pour l'instant, cette option n'est pas disponible.


32
Eh bien, la première chose qui vous vient à l'esprit (à moins que le compilateur ne l'optimise lui-même) est de remplacer tous ceux pow(l1 * l2 * l3, -0.1e1 / 0.3e1)par une variable ... Vous devez cependant comparer votre code pour être sûr qu'il fonctionne rapidement ou lentement.
SingerOfTheFall

6
Formatez également le code pour le rendre plus lisible - peut aider à identifier les possibilités d'amélioration.
Ed Heal

26
Pourquoi tous les votes contre et tous les votes pour clôturer? Pour ceux d'entre vous qui n'aiment pas la programmation numérique ou scientifique, allez voir d'autres questions. C'est une bonne question qui convient bien à ce site. Le site scicomp est toujours en version bêta; migration il n'y a pas une bonne option. Le site de révision de code n'a pas assez d'yeux sciomp. Ce que l'OP a fait arrive assez souvent dans le calcul scientifique: Construisez un problème dans un programme mathématique symbolique, demandez au programme de générer du code, et ne touchez pas au résultat car le code généré est un tel gâchis.
David Hammen

6
@DavidHammen, le site de révision de code n'a pas assez d'yeux sciomp - sonne comme un problème de poule et d'oeuf, et un état d'esprit qui n'aide pas CR à avoir plus de tels yeux. Il en va de même pour l'idée de refuser le site bêta de scicomp parce qu'il est bêta - si tout le monde pensait comme ça, le seul site à se développer serait Stack Overflow.
Mathieu Guindon

13
Cette question est en cours de discussion sur la méta ici
NathanOliver

Réponses:


88

Modifier le résumé

  • Ma réponse originale a simplement noté que le code contenait beaucoup de calculs répliqués et que de nombreuses puissances impliquaient des facteurs de 1/3. Par exemple, pow(x, 0.1e1/0.3e1)est le même que cbrt(x).
  • Ma deuxième modification était tout simplement erronée et ma troisième extrapolait cette erreur. C'est ce qui fait peur aux gens de changer les résultats de type oracle des programmes de mathématiques symboliques qui commencent par la lettre «M». J'ai rayées (c. -à- grève ) ces modifications et les a poussés au fond 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.
  • Mon 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, l2et l3sont des nombres réels positifs et si aun nombre réel non nul. (Nous n'avons pas encore entendu le PO concernant la nature spécifique de ces coefficients. Compte tenu de la nature du problème, il s'agit d'hypothèses raisonnables.)
  • Cette modification tente de répondre au problème générique de la simplification de ces expressions.

Tout d'abord

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

Maple et Mathematica passent parfois à côté de l'évidence. Plus important encore, les utilisateurs de Maple et Mathematica font parfois des erreurs. Substituer «souvent», ou peut-être même «presque toujours», au lieu de «parfois est probablement plus proche de la cible.

Vous auriez pu aider Maple à simplifier cette expression en lui indiquant les paramètres en question. Dans l'exemple en question, je soupçonne que l1, l2et l3sont des nombres réels positifs et c'est aun nombre réel différent de zéro. Si c'est le cas, dites-le-lui. Ces programmes de 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 qui ne sont pas valides dans les nombres complexes.


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

Les programmes de 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 à portée de main, vous auriez pu Maple simplifier cette expression en lui disant que l1, l2et l3sont des nombres réels positifs et aest un nombre réel non nul. Si c'est le cas, dites-le-lui. Ces programmes de 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 telles que a x b x = (ab) x . Ce n'est que si aet bsont des nombres réels positifs et si xest réel. Ce n'est pas valable dans les nombres complexes.

En fin de compte, ces programmes mathématiques symboliques suivent des algorithmes. Aidez-le. Essayez de jouer avec l'expansion, la collecte et la simplification avant de générer du code. Dans ce cas, vous auriez pu collecter les termes impliquant un facteur de muet ceux impliquant un facteur de K. Réduire une expression à sa «forme la plus simple» reste un peu un art.

Lorsque vous obtenez un horrible désordre de code généré, ne l'acceptez pas comme une vérité à ne 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 la réponse de Walter a poussé la mienne plusieurs pas 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 a donnée.


À propos de la question spécifique

Vous faites beaucoup d'addition et de soustraction dans ce calcul. Vous pouvez avoir de graves problèmes si vous avez des termes qui s'annulent presque mutuellement. Vous gaspillez beaucoup de CPU si vous avez un terme qui domine les autres.

Ensuite, vous gaspillez beaucoup de CPU en effectuant des calculs répétés. Sauf si vous avez activé -ffast-math, ce qui permet au compilateur de briser certaines des règles de la virgule flottante IEEE, le compilateur ne simplifiera pas (en fait, ne doit pas) simplifier cette expression pour vous. Il fera plutôt exactement ce que vous lui avez dit de faire. Au minimum, vous devez calculer l1 * l2 * l3avant de calculer ce désordre.

Enfin, vous passez beaucoup d'appels vers 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 à powpourraient ê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, l2et l3sont des nombres réels plutôt que des nombres complexes, et que la racine cubique réelle (plutôt que la racine complexe principale) doit être extraite, ce qui précède 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_l123au 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, mais simplifiez toujours aussi.


Voici quelques-unes 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 c'est frappé. C'est faux.

Mettre à 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, l2et l3sont des nombres réels plutôt que des nombres complexes, et que la racine cubique réelle (plutôt que la racine complexe principale) doit être extraite, ce qui précède 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 le calcul (rien ne garantit que j'ai bien fait le calcul), la mauvaise expression 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, l2et l3sont des nombres réels positifs.


2
Eh bien, l'élimination du CSE devrait fonctionner, indépendamment de la sémantique assouplie (et de l'OP indiqué dans les commentaires). Bien sûr, si cela compte (mesuré), cela devrait être inspecté (assemblage généré). Vos points sur les termes dominants, les simplifications de formule manquées, les fonctions mieux spécialisées et les dangers d'annulation sont très bons.
Deduplicator

3
@Deduplicator - Pas avec virgule flottante. À moins que l'on n'active les optimisations mathématiques non sécurisées (par exemple, en spécifiant -ffast-mathavec gcc ou clang), le compilateur ne peut pas se fier à pow(x,-1.0/3.0)être égal à x*pow(x,-4.0/3.0). Ce dernier pourrait être sous-jacent alors que le premier pourrait ne pas l'être. Pour être conforme à la norme en virgule flottante, le compilateur ne doit pas optimiser ce calcul à zéro.
David Hammen

Eh bien, ceux-ci sont bien plus ambitieux que tout ce que je voulais dire.
Deduplicator

1
@Deduplicator: Comme je l'ai commenté sur une autre réponse : vous avez besoin -fno-math-errnod' powappels identiques de g ++ à CSE . (À moins que cela puisse prouver que pow n'aura pas besoin de définir errno?)
Peter Cordes

1
@Lefti - Prenez beaucoup à la réponse de Walter. C'est beaucoup plus rapide. Il y a un problème potentiel avec toutes ces réponses, qui est l'annulation numérique. En supposant que N1, N2et N3sont non négatifs, l'un des 2*N_i-(N_j+N_k)sera négatif, l'un sera positif et l'autre sera quelque part entre les deux. Cela peut facilement entraîner des problèmes d'annulation numériques.
David Hammen

32

La première chose à noter est que powcela coûte 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 du pré-calcul de 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 en avez plus powavec l'exposant a. Si aest un entier et connu au moment du compilateur, vous pouvez également les remplacer par boost::math::pow<a>(...)pour améliorer les performances. Je suggérerais également de remplacer des termes comme a / l1 / 0.3e1par a / (l1 * 0.3e1)car la multiplication est plus rapide que la division.

Enfin, si vous utilisez g ++, vous pouvez utiliser l' -ffast-mathindicateur qui permet à l'optimiseur d'être plus agressif dans la transformation des équations. Découvrez ce que fait réellement ce drapeau , car il a cependant des effets secondaires.


5
Dans notre code, l'utilisation -ffast-mathconduit le code à devenir instable ou à donner de mauvaises réponses. Nous avons un problème similaire avec les compilateurs Intel et devons utiliser l' -fp-model preciseoption, sinon le code explose ou donne les mauvaises réponses. -ffast-mathCela pourrait donc l' accélérer, mais je recommanderais de procéder très prudemment avec cette option, en plus des effets secondaires répertoriés dans votre question liée.
tpg2114

2
@ tpg2114: D'après mes tests, vous n'avez besoin que-fno-math-errno de g ++ pour pouvoir hisser des appels identiques powhors d'une boucle. C'est la partie la moins «dangereuse» de -ffast-math, pour la plupart du code.
Peter Cordes

1
@PeterCordes Ce sont des résultats intéressants! Nous avons également eu des problèmes avec la pow lenteur extrême et avons fini par utiliser le dlsymhack mentionné dans les commentaires pour obtenir des augmentations de performances considérables alors que nous pouvions le faire avec un peu moins de précision.
tpg2114

GCC ne comprendrait-il pas que le pow est une fonction pure? C'est probablement une connaissance intégrée.
usr

6
@usr: C'est juste le point, je pense. pown'est pas une fonction pure, selon la norme, car elle est censée se définir errnodans certaines circonstances. Définir des indicateurs tels que le -fno-math-errnofait ne pas le définir errno(violant ainsi la norme), mais c'est alors une fonction pure et peut être optimisé en tant que tel.
Nate Eldredge

20

Woah, quelle putain d'expression. Créer l'expression avec Maple était en fait un choix sous-optimal ici. Le résultat est tout simplement illisible.

  1. choisissez des noms de variables parlantes (pas l1, l2, l3, mais par exemple hauteur, largeur, profondeur, si c'est ce qu'ils signifient). Ensuite, il est plus facile pour vous 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 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 le peut pas - par exemple lorsque l'imbrication de boucle s'étend sur plusieurs fonctions dans différentes unités de compilation. Quoi qu'il en soit, cela vous donnera un code bien meilleur lisible, compréhensible et maintenable.


8
"le compilateur devrait le faire, mais parfois non", est la clé ici. outre la lisibilité, bien sûr.
Javier

3
Si le compilateur n'est pas obligé de faire quelque chose, alors supposer que c'est presque toujours faux.
edmz

4
Re Choisissez des noms de variables parlantes - Souvent, cette belle règle ne s'applique pas lorsque vous faites des maths. Quand on regarde du code censé implémenter un algorithme dans une revue scientifique, je préfère de beaucoup voir les symboles dans le code être exactement ceux utilisés dans la revue. En règle générale, cela signifie des noms extrêmement courts, éventuellement avec un indice.
David Hammen

8
«Le résultat est tout simplement illisible» - pourquoi est-ce un problème? Vous ne vous soucieriez pas que la sortie de langage de haut niveau d'un générateur de lexer ou d'analyseur soit "illisible" (par les humains). Ce qui compte ici, c'est que l' entrée du générateur de code (Maple) soit lisible et vérifiable. La chose à ne pas faire est de modifier manuellement le code généré, si vous voulez être sûr qu'il est sans erreur.
alephzero

3
@DavidHammen: Eh bien, dans ce cas, ceux à une lettre sont les "noms parlants". Par exemple, lorsque vous faites de la géométrie dans un système de coordonnées cartésien bidimensionnel, xet ney sont pas des variables à une seule lettre sans signification, ce sont des mots entiers avec une définition précise et une signification bien et largement comprise.
Jörg W Mittag

17

La réponse de David Hammen est bonne, mais encore loin d'être optimale. 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 l'exploitation de certaines identités mathématiques. Répétons cela é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*N1vers N1+N1etc. 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);

Comme 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 était l'appel à cbrt(), que nous avons éliminé).

Si par hasard aest un entier, les appels à powpourraient être optimisés pour les appels à cbrt(plus les puissances entières), ou si athirdest un demi-entier, nous pouvons utiliser sqrt(plus les puissances entières). En outre, si par hasard l1==l2ou l1==l3ou l2==l3un ou les deux appels à powpeut être éliminé. Donc, cela vaut la peine de les considérer comme des cas spéciaux si de telles chances existent de manière réaliste.


@gnat J'apprécie votre montage (j'ai pensé le faire moi-même), mais je l'aurais trouvé plus juste, si la réponse de David était également liée à celle-ci. Pourquoi n'éditez-vous pas également la réponse de David de la même manière?
Walter

1
Je n'ai édité que parce que je vous ai vu le mentionner explicitement; J'ai relu la réponse de David et je n'ai pas trouvé de référence à votre réponse là-bas. J'essaie d'éviter les modifications où il n'est pas clair à 100% que ce que j'ajoute correspond aux intentions de l'auteur
gnat

1
@Walter - Ma réponse renvoie maintenant à la vôtre.
David Hammen

1
Ce n'était certainement pas moi. J'ai voté pour votre réponse il y a quelques jours. J'ai également reçu un vote défavorable aléatoire sur ma réponse. Les choses arrivent parfois.
David Hammen

1
Vous et moi avons reçu un vote négatif dérisoire chacun. Regardez tous les votes négatifs sur la question! À partir de maintenant, la question a reçu 16 votes négatifs. Il a également reçu 80 votes positifs qui ont plus que compensé tous ces votes négatifs.
David Hammen

12
  1. Combien font «plusieurs»?
  2. Combien de temps cela prend-il?
  3. Do TOUS les paramètres changent entre 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, j'aimerais 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 travaillé un peu plus sur la dernière formule à trois lignes et je l'ai ramenée à cette beauté:

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

Laissez-moi vous 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)

2
C'est perceptible, hein? :) FORTRAN, IIRC, a été conçu pour des calculs de formule efficaces ("FOR" est pour formule).
Vlad Feinstein

La plupart des codes F77 que j'ai vus ressemblaient à ça (par exemple, BLAS et NR). Très heureux Fortran 90-> 2008 existe :)
Kyle Kanos

Oui. Si vous traduisez une formule, quel meilleur moyen que FORmulaTRANslation?
Brian Drummond

1
Votre «optimisation» attaque le mauvais endroit. Les bits coûteux sont les appels à std::pow(), dont vous avez encore 6, 3 fois plus que nécessaire. En d'autres termes, votre code est 3 fois plus lent que possible.
Walter

7

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 se réécrit essentiellement ax^3 + bx^2 + cx + dcomme d + x(c + x(b + x(a))). Cela vous évitera beaucoup d'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 vous 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 aide pour cela, car x^20et xsont généralement séparés de plusieurs ordres de grandeur).

Aussi comme conseil pratique, si vous ne l'avez pas déjà fait, essayez d'abord de simplifier l'expression en érable. Vous pouvez probablement lui faire faire la plupart de l'élimination des sous-expressions courantes 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.


La forme Horner est assez standard pour coder les polynômes et cela n'a aucun rapport avec la question.
Walter

1
Cela peut être vrai compte tenu de son exemple, mais vous remarquerez qu'il a dit «équations de ce type». J'ai pensé que la réponse serait utile si l'affiche avait des polynômes dans son système. J'ai particulièrement remarqué que les générateurs de code pour les programmes CAS tels que Mathematica et Maple ont tendance à NE PAS vous donner la forme Horner à moins que vous ne le demandiez spécifiquement; ils utilisent par défaut la manière dont vous écririez généralement un polynôme en tant qu'être humain.
neocpp

3

Il semble que vous ayez de nombreuses 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 powfonction qui peut être coûteuse.

Vous pouvez également pré-calibrer

l1 * l2 * l3

comme vous utilisez ce terme à plusieurs reprises.


6
Je parie que l'optimiseur le fait déjà pour vous ... même si cela rend au moins le code plus lisible.
Karoly Horvath

Je l'ai fait, mais cela n'a pas du tout accéléré les choses. J'ai pensé que c'était parce que l'optimisation du compilateur s'en occupait déjà.

le stockage de l1 * l2 * l3 accélère cependant les choses,

parce que le compilateur ne peut parfois pas faire certaines optimisations ou les trouver en conflit avec d'autres options.
Javier

1
En fait, le compilateur ne doit pas effectuer ces optimisations à moins d' -ffast-mathêtre activé, et comme indiqué dans un commentaire de @ tpg2114, cette optimisation peut créer des résultats extrêmement instables.
David Hammen

0

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

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

Sinon, vous souhaiterez peut-être envisager plusieurs threads pour les calculs.


10
Cette réponse est orthogonale à la question posée. Bien que les GPU aient beaucoup de processeurs, ils sont assez lents par rapport au FPU intégré au CPU. Effectuer un seul calcul en série avec un GPU est une grosse perte. Le CPU doit remplir le pipeline vers le GPU, attendre que le GPU lent effectue cette tâche unique, puis décharger le résultat. Alors que les GPU sont absolument fantastiques lorsque le problème en question est massivement parallélisable, ils sont absolument atroces lorsqu'il s'agit d'effectuer des tâches série.
David Hammen

1
Dans la question d'origine: "Comme ce code est exécuté plusieurs fois, les performances sont un problème.". C'est un de plus que «plusieurs». L'op peut envoyer les calculs de manière filetée.
user3791372

0

Par hasard, pourriez-vous fournir le calcul symboliquement. S'il y a des opérations vectorielles, vous voudrez peut-être vraiment étudier en utilisant blas ou lapack qui, dans certains cas, peuvent 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 du possible, vos calculs pourraient être plus lisibles.


0

Comme vous l'avez explicitement demandé sur les optimisations de haut niveau, il pourrait être intéressant d'essayer différents compilateurs C ++. De nos jours, les compilateurs sont des bêtes d'optimisation très complexes et les fournisseurs de processeurs 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 à ceux de 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 donner de bons résultats sur les architectures AMD

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

Bonne chance!

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.