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*l3
par 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.
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.Réponses:
Modifier le résumé
pow(x, 0.1e1/0.3e1)
est le même quecbrt(x)
.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.l1
,l2
etl3
sont des nombres réels positifs et sia
un 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.)Tout d'abord
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
,l2
etl3
sont des nombres réels positifs et c'esta
un 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
,l2
etl3
sont des nombres réels positifs eta
est 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 sia
etb
sont des nombres réels positifs et six
est 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
mu
et ceux impliquant un facteur deK
. 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 calculerl1 * l2 * l3
avant 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 àpow
pourraient être effectués avec un seul appel àstd::cbrt
:Avec ça,
X * pow(l1 * l2 * l3, 0.1e1 / 0.3e1)
devientX * l123_pow_1_3
.X * pow(l1 * l2 * l3, -0.1e1 / 0.3e1)
devientX / l123_pow_1_3
.X * pow(l1 * l2 * l3, 0.4e1 / 0.3e1)
devientX * l123_pow_4_3
.X * pow(l1 * l2 * l3, -0.4e1 / 0.3e1)
devientX / l123_pow_4_3
.Maple a raté l'évidence.
Par exemple, il existe un moyen beaucoup plus simple d'écrire
En supposant que
l1
,l2
etl3
sont 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 àou
En utilisant
cbrt_l123
au lieu del123_pow_1_3
, l'expression désagréable de la question se réduit àVérifiez toujours, mais simplifiez toujours aussi.
Voici quelques-unes de mes étapes pour arriver à ce qui précède:
Mauvaise réponse, intentionnellement gardée pour l'humilité
Notez que c'est frappé. C'est faux.
Mettre à jourMaple a raté l'évidence. Par exemple, il existe un moyen beaucoup plus simple d'écrire
En supposant que
l1
,l2
etl3
sont 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 à
Ce qui précède suppose quel1
,l2
etl3
sont des nombres réels positifs.la source
-ffast-math
avec 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.-fno-math-errno
d'pow
appels identiques de g ++ à CSE . (À moins que cela puisse prouver que pow n'aura pas besoin de définir errno?)N1
,N2
etN3
sont non négatifs, l'un des2*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.La première chose à noter est que
pow
cela 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 depow(l1 * l2 * l3, -0.1e1 / 0.3e1)
etpow(l1 * l2 * l3, -0.4e1 / 0.3e1)
. Je m'attendrais donc à un gros gain du pré-calcul de ceux-ci:où j'utilise la fonction boost pow .
De plus, vous en avez plus
pow
avec l'exposanta
. Sia
est un entier et connu au moment du compilateur, vous pouvez également les remplacer parboost::math::pow<a>(...)
pour améliorer les performances. Je suggérerais également de remplacer des termes commea / l1 / 0.3e1
para / (l1 * 0.3e1)
car la multiplication est plus rapide que la division.Enfin, si vous utilisez g ++, vous pouvez utiliser l'
-ffast-math
indicateur 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.la source
-ffast-math
conduit 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 precise
option, sinon le code explose ou donne les mauvaises réponses.-ffast-math
Cela 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.-fno-math-errno
de g ++ pour pouvoir hisser des appels identiquespow
hors d'une boucle. C'est la partie la moins «dangereuse» de -ffast-math, pour la plupart du code.pow
lenteur extrême et avons fini par utiliser ledlsym
hack 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.pow
n'est pas une fonction pure, selon la norme, car elle est censée se définirerrno
dans certaines circonstances. Définir des indicateurs tels que le-fno-math-errno
fait ne pas le définirerrno
(violant ainsi la norme), mais c'est alors une fonction pure et peut être optimisé en tant que tel.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.
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.
la source
x
et 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.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)
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.Notez que j'ai également optimisé
2.0*N1
versN1+N1
etc. Ensuite, nous pouvons faire avec seulement deux appels àpow()
.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
a
est un entier, les appels àpow
pourraient être optimisés pour les appels àcbrt
(plus les puissances entières), ou siathird
est un demi-entier, nous pouvons utilisersqrt
(plus les puissances entières). En outre, si par hasardl1==l2
oul1==l3
oul2==l3
un ou les deux appels àpow
peut ê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.la source
J'ai tenté une simplification manuelle de cette formule, j'aimerais savoir si elle enregistre quelque chose?
[AJOUTÉ] J'ai travaillé un peu plus sur la dernière formule à trois lignes et je l'ai ramenée à cette beauté:
Laissez-moi vous montrer mon travail, étape par étape:
la source
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.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 + d
commed + x(c + x(b + x(a)))
. Cela vous évitera beaucoup d'appels répétéspow()
et vous empêchera de faire des choses idiotes comme appeler séparémentpow(x,6)
etpow(x,7)
au lieu de simplement fairex*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^20
etx
sont 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 source
Il semble que vous ayez de nombreuses opérations répétées en cours.
Vous pouvez les pré-calculer afin de ne pas appeler à plusieurs reprises la
pow
fonction qui peut être coûteuse.Vous pouvez également pré-calibrer
comme vous utilisez ce terme à plusieurs reprises.
la source
-ffast-math
être activé, et comme indiqué dans un commentaire de @ tpg2114, cette optimisation peut créer des résultats extrêmement instables.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.
la source
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.
la source
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).
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!
la source