Ich entwickle einige technische Simulationen. Dies beinhaltet die Implementierung einiger langer Gleichungen wie dieser Gleichung, um die Spannung in einem gummiartigen Material zu berechnen:
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;
Ich verwende Maple, um den C ++ - Code zu generieren, um Fehler zu vermeiden (und Zeit mit langwieriger Algebra zu sparen). Da dieser Code tausende (wenn nicht millionenfach) ausgeführt wird, ist die Leistung ein Problem. Leider vereinfacht sich die Mathematik bisher nur; Die langen Gleichungen sind unvermeidlich.
Welchen Ansatz kann ich wählen, um diese Implementierung zu optimieren? Ich suche nach Strategien auf hoher Ebene, die ich bei der Implementierung solcher Gleichungen anwenden sollte, nicht unbedingt nach spezifischen Optimierungen für das oben gezeigte Beispiel.
Ich kompiliere mit g ++ mit --enable-optimize=-O3
.
Aktualisieren:
Ich weiß, dass es viele wiederholte Ausdrücke gibt. Ich gehe davon aus, dass der Compiler damit umgehen würde. Meine bisherigen Tests legen nahe, dass dies der Fall ist.
l1, l2, l3, mu, a, K
sind alle positiven reellen Zahlen (nicht Null).
Ich habe durch l1*l2*l3
eine äquivalente Variable ersetzt : J
. Dies hat zur Verbesserung der Leistung beigetragen.
Ersetzen pow(x, 0.1e1/0.3e1)
durch cbrt(x)
war ein guter Vorschlag.
Dies wird auf CPUs ausgeführt. In naher Zukunft wird dies wahrscheinlich besser auf GPUs ausgeführt, aber diese Option ist derzeit nicht verfügbar.
pow(l1 * l2 * l3, -0.1e1 / 0.3e1)
durch eine Variable zu ersetzen ... Sie müssen Ihren Code vergleichen, um sicherzugehen, ob er schnell oder langsam läuft.Antworten:
Zusammenfassung bearbeiten
pow(x, 0.1e1/0.3e1)
ist das gleiche wiecbrt(x)
.gestrichen) und sie an den Grund der aktuellen Überarbeitung dieser Antwort verschoben. Ich habe sie jedoch nicht gelöscht. Ich bin menschlich. Es fällt uns leicht, einen Fehler zu machen.l1
,l2
undl3
positive reelle Zahlen sind und wenna
eine Nicht-Null reelle Zahl. (Wir haben vom OP noch nichts über die Spezifität dieser Koeffizienten gehört. Angesichts der Art des Problems sind dies vernünftige Annahmen.)Das wichtigste zuerst
Maple und Mathematica vermissen manchmal das Offensichtliche. Noch wichtiger ist, dass die Benutzer von Maple und Mathematica manchmal Fehler machen. Das Ersetzen von "oft" oder vielleicht sogar "fast immer" anstelle von "manchmal" ist wahrscheinlich näher an der Marke.
Sie hätten Maple helfen können, diesen Ausdruck zu vereinfachen, indem Sie ihm die fraglichen Parameter mitteilen. Im Beispiel auf der Hand, vermute ich , dass
l1
,l2
undl3
positive reelle Zahlen sind und dasa
ist eine von Null verschiedene reelle Zahl. Wenn das der Fall ist, sagen Sie es das. Diese symbolischen Mathematikprogramme gehen normalerweise davon aus, dass die vorliegenden Größen komplex sind. Durch die Einschränkung der Domäne kann das Programm Annahmen treffen, die in den komplexen Zahlen nicht gültig sind.Wie man diese großen Probleme mit symbolischen Mathematikprogrammen vereinfacht (diese Bearbeitung)
Symbolische Mathematikprogramme bieten normalerweise die Möglichkeit, Informationen über die verschiedenen Parameter bereitzustellen. Verwenden Sie diese Fähigkeit, insbesondere wenn Ihr Problem Teilung oder Potenzierung beinhaltet. Im Beispiel auf der Hand, hätte Ihnen geholfen Maple diesen Ausdruck zu vereinfachen , indem sie das sagen
l1
,l2
undl3
positive reelle Zahlen sind und dasa
ist eine von Null verschiedene reelle Zahl. Wenn das der Fall ist, sagen Sie es das. Diese symbolischen Mathematikprogramme gehen normalerweise davon aus, dass die vorliegenden Größen komplex sind. Durch Einschränken der Domäne kann das Programm Annahmen wie a x b x = (ab) x treffen . Dies ist nur dann, wenna
undb
positive reelle Zahlen sind und wennx
es real ist. Es ist in den komplexen Zahlen nicht gültig.Letztendlich folgen diese symbolischen Mathematikprogramme Algorithmen. Helfen Sie mit. Versuchen Sie, mit dem Erweitern, Sammeln und Vereinfachen zu spielen, bevor Sie Code generieren. In diesem Fall hätten Sie die Begriffe mit einem Faktor von
mu
und die mit einem Faktor von sammeln könnenK
. Das Reduzieren eines Ausdrucks auf seine "einfachste Form" bleibt eine Kunst.Wenn Sie ein hässliches Durcheinander von generiertem Code erhalten, akzeptieren Sie ihn nicht als eine Wahrheit, die Sie nicht berühren dürfen. Versuchen Sie es selbst zu vereinfachen. Schauen Sie sich an, was das symbolische Mathematikprogramm hatte, bevor es Code generierte. Schau dir an, wie ich deinen Ausdruck auf etwas viel Einfacheres und viel Schnelleres reduziert habe und wie Walters Antwort meine einige Schritte weiter gebracht hat. Es gibt kein Zauberrezept. Wenn es ein magisches Rezept gegeben hätte, hätte Maple es angewendet und die Antwort gegeben, die Walter gegeben hat.
Über die spezifische Frage
Sie addieren und subtrahieren viel in dieser Berechnung. Sie können in große Schwierigkeiten geraten, wenn Sie Begriffe haben, die sich fast gegenseitig aufheben. Sie verschwenden viel CPU, wenn Sie einen Begriff haben, der die anderen dominiert.
Als nächstes verschwenden Sie viel CPU, indem Sie wiederholte Berechnungen durchführen. Sofern Sie nicht aktiviert haben
-ffast-math
, wodurch der Compiler einige der Regeln des IEEE-Gleitkommas brechen kann, wird der Compiler diesen Ausdruck für Sie nicht (in der Tat nicht) vereinfachen. Es wird stattdessen genau das tun, was Sie ihm gesagt haben. Sie sollten mindestens rechnen,l1 * l2 * l3
bevor Sie dieses Durcheinander berechnen .Schließlich telefonieren Sie viel
pow
, was extrem langsam ist. Beachten Sie, dass einige dieser Aufrufe die Form (l1 * l2 * l3) (1/3) haben . Viele dieser Anrufe anpow
könnten mit einem einzigen Anruf an ausgeführt werdenstd::cbrt
:Mit diesem,
X * pow(l1 * l2 * l3, 0.1e1 / 0.3e1)
wirdX * l123_pow_1_3
.X * pow(l1 * l2 * l3, -0.1e1 / 0.3e1)
wirdX / l123_pow_1_3
.X * pow(l1 * l2 * l3, 0.4e1 / 0.3e1)
wirdX * l123_pow_4_3
.X * pow(l1 * l2 * l3, -0.4e1 / 0.3e1)
wirdX / l123_pow_4_3
.Maple vermisste das Offensichtliche.
Zum Beispiel gibt es eine viel einfachere Möglichkeit zu schreiben
Vorausgesetzt, das
l1
,l2
undl3
sind real und nicht komplexe Zahlen, und dass der reale Kubikwurzel ( und nicht das Prinzip komplexe root) extrahiert werden sollen, verringert sich die obenoder
Verwenden
cbrt_l123
stattl123_pow_1_3
reduziert sich der böse Ausdruck in der Frage aufImmer überprüfen, aber auch immer vereinfachen.
Hier sind einige meiner Schritte, um zu den oben genannten Ergebnissen zu gelangen:
Falsche Antwort, absichtlich aus Demut gehalten
Beachten Sie, dass dies betroffen ist. Es ist falsch.
AktualisierenMaple vermisste das Offensichtliche. Zum Beispiel gibt es eine viel einfachere Möglichkeit zu schreiben
Vorausgesetzt, das
l1
,l2
undl3
sind real und nicht komplexe Zahlen, und dass der reale Kubikwurzel ( und nicht das Prinzip komplexe root) extrahiert werden sollen, verringert sich die oben auf Null. Diese Berechnung von Null wird um ein Vielfaches wiederholt.Zweites Update
Wenn ich die Mathematik richtig gemacht habe (es gibt keine Garantie dafür, dass ich die Mathematik richtig gemacht habe), reduziert sich der böse Ausdruck in der Frage auf
Die oben geht davon aus, dassl1
,l2
undl3
positive reelle Zahlen sind .quelle
-ffast-math
mit gcc oder clang), kann sich der Compiler nicht darauf verlassen,pow(x,-1.0/3.0)
dass er gleich istx*pow(x,-4.0/3.0)
. Letzteres könnte unterlaufen, während das erste nicht. Um dem Gleitkomma-Standard zu entsprechen, darf der Compiler diese Berechnung nicht auf Null optimieren.-fno-math-errno
für g ++ zu CSE identischepow
Aufrufe. (Es sei denn, es kann vielleicht beweisen, dass pow nicht errno setzen muss?)N1
sindN2
undN3
nicht negativ, einer der2*N_i-(N_j+N_k)
ist negativ, einer ist positiv und der andere liegt irgendwo dazwischen. Dies kann leicht zu numerischen Löschproblemen führen.Als erstes ist zu beachten, dass dies
pow
sehr teuer ist. Sie sollten dies also so weit wie möglich beseitigen. Beim Durchsuchen des Ausdrucks sehe ich viele Wiederholungen vonpow(l1 * l2 * l3, -0.1e1 / 0.3e1)
undpow(l1 * l2 * l3, -0.4e1 / 0.3e1)
. Ich würde also einen großen Gewinn erwarten, wenn ich Folgendes vorberechnete:wo ich die Boost Pow Funktion benutze .
Außerdem haben Sie noch mehr
pow
mit Exponenta
. Wenna
Integer ist und zur Compilerzeit bekannt ist, können Sie diese auch durch ersetzenboost::math::pow<a>(...)
, um weitere Leistung zu erzielen. Ich würde auch vorschlagen, Begriffe wiea / l1 / 0.3e1
durch zu ersetzen,a / (l1 * 0.3e1)
da die Multiplikation schneller ist als die Division.Wenn Sie g ++ verwenden, können Sie schließlich das
-ffast-math
Flag verwenden, mit dem der Optimierer bei der Transformation von Gleichungen aggressiver vorgehen kann. Lesen Sie, was diese Flagge tatsächlich tut , da sie jedoch Nebenwirkungen hat.quelle
-ffast-math
führt die Verwendung dazu, dass der Code instabil wird oder falsche Antworten gibt. Wir haben ein ähnliches Problem mit Intel-Compilern und müssen die-fp-model precise
Option verwenden, andernfalls explodiert der Code oder gibt die falschen Antworten. Könnte-ffast-math
es also beschleunigen, aber ich würde empfehlen, zusätzlich zu den in Ihrer verknüpften Frage aufgeführten Nebenwirkungen sehr vorsichtig mit dieser Option umzugehen.-fno-math-errno
g ++ nur identische Aufrufepow
aus einer Schleife herausheben können . Das ist für den meisten Code der am wenigsten "gefährliche" Teil von -ffast-math.pow
, extrem langsam zu sein, und haben schließlich dendlsym
in den Kommentaren erwähnten Hack verwendet, um erhebliche Leistungssteigerungen zu erzielen, wenn wir dies tatsächlich mit etwas weniger Präzision tun könnten.pow
ist nach dem Standard keine reine Funktion, da sie untererrno
bestimmten Umständen eingestellt werden soll. Das Setzen von Flags wie z. B.-fno-math-errno
bewirkt, dass es nicht gesetzt wirderrno
(was gegen den Standard verstößt), aber dann ist es eine reine Funktion und kann als solche optimiert werden.Woah, was für ein verdammter Ausdruck. Das Erstellen des Ausdrucks mit Maple war hier tatsächlich eine suboptimale Wahl. Das Ergebnis ist einfach unlesbar.
Theoretisch sollte der Compiler in der Lage sein, all das für Sie zu tun, aber manchmal kann er dies nicht - z. B. wenn sich die Schleifenverschachtelung über mehrere Funktionen in verschiedenen Kompilierungseinheiten erstreckt. Auf diese Weise erhalten Sie einen viel besser lesbaren, verständlichen und wartbaren Code.
quelle
x
undy
ist nicht sinnlos aus einem Buchstaben Variablen, sie heil sind Worte mit einer genauen Definition und eine gut und weit verstandenen Bedeutung.Die Antwort von David Hammen ist gut, aber noch lange nicht optimal. Fahren wir mit seinem letzten Ausdruck fort (zum Zeitpunkt des Schreibens)
was weiter optimiert werden kann. Insbesondere können wir den Aufruf von
cbrt()
und einen der Aufrufe von vermeiden ,pow()
wenn wir einige mathematische Identitäten ausnutzen. Lassen Sie uns dies Schritt für Schritt wiederholen.Beachten Sie, dass ich auch
2.0*N1
aufN1+N1
usw. optimiert habe . Als nächstes können wir mit nur zwei Aufrufen auf tunpow()
.Da die Anrufe
pow()
hier bei weitem die teuerste Operation sind, lohnt es sich, sie so weit wie möglich zu reduzieren (die nächste kostspielige Operation war der Anruf beicbrt()
, den wir eliminiert haben).Wenn zufällig
a
eine Ganzzahl ist, können die Aufrufe anpow
für Aufrufe ancbrt
(plus ganzzahlige Potenzen) optimiert werden , oder wennathird
es sich um eine halbe Ganzzahl handelt, können wirsqrt
(plus ganzzahlige Potenzen) verwenden. Darüber hinaus kann, wenn durch Zufalll1==l2
oderl1==l3
oderl2==l3
ein oder beide Anrufepow
zu eliminiert werden. Es lohnt sich also, diese als Sonderfälle zu betrachten, wenn solche Chancen realistisch sind.quelle
Ich habe versucht, diese Formel manuell zu vereinfachen. Möchten Sie wissen, ob sie etwas spart?
[HINZUGEFÜGT] Ich habe noch etwas an der letzten dreizeiligen Formel gearbeitet und es auf diese Schönheit zurückgeführt:
Lassen Sie mich Schritt für Schritt meine Arbeit zeigen:
quelle
std::pow()
, von denen Sie noch 6, 3 mal mehr als nötig haben. Mit anderen Worten, Ihr Code ist dreimal langsamer als möglich.Dies mag etwas knapp sein, aber ich habe tatsächlich eine gute Beschleunigung für Polynome (Interpolation von Energiefunktionen) gefunden, indem ich Horner Form verwendet habe, das im Grunde genommen
ax^3 + bx^2 + cx + d
als umschreibtd + x(c + x(b + x(a)))
. Dies vermeidet viele wiederholte Anrufe beipow()
und hindert Sie daran, dumme Dinge wie separates Anrufenpow(x,6)
undpow(x,7)
nicht nur zu tunx*pow(x,6)
.Dies gilt nicht direkt für Ihr aktuelles Problem. Wenn Sie jedoch Polynome höherer Ordnung mit ganzzahligen Potenzen haben, kann dies hilfreich sein. Möglicherweise müssen Sie auf numerische Stabilitäts- und Überlaufprobleme achten, da die Reihenfolge der Operationen dafür wichtig ist (obwohl ich im Allgemeinen tatsächlich denke, dass Horner Form dabei hilft, da
x^20
undx
normalerweise viele Größenordnungen voneinander entfernt sind).Versuchen Sie auch als praktischen Tipp, wenn Sie dies noch nicht getan haben, zuerst den Ausdruck in Ahorn zu vereinfachen. Sie können es wahrscheinlich dazu bringen, den größten Teil der üblichen Eliminierung von Unterausdrücken für Sie durchzuführen. Ich weiß nicht, wie sehr sich dies insbesondere auf den Codegenerator in diesem Programm auswirkt, aber ich weiß, dass in Mathematica eine vollständige Vereinfachung vor dem Generieren des Codes zu einem großen Unterschied führen kann.
quelle
Es sieht so aus, als würden viele wiederholte Operationen durchgeführt.
Sie können diese vorberechnen, damit Sie die
pow
Funktion nicht wiederholt aufrufen , was teuer sein kann.Sie können auch vorkalutieren
wie Sie diesen Begriff wiederholt verwenden.
quelle
-ffast-math
aktiviert sind. Wie in einem Kommentar von @ tpg2114 erwähnt, kann diese Optimierung zu äußerst instabilen Ergebnissen führen.Wenn Sie eine Nvidia CUDA-Grafikkarte besitzen, können Sie die Berechnungen auf die Grafikkarte verlagern, die sich selbst besser für rechenintensive Berechnungen eignet.
https://developer.nvidia.com/how-to-cuda-c-cpp
Wenn nicht, können Sie mehrere Threads für Berechnungen berücksichtigen.
quelle
Könnten Sie die Berechnung zufällig symbolisch angeben? Wenn es Vektoroperationen gibt, möchten Sie möglicherweise wirklich die Verwendung von Blas oder Lapack untersuchen, die in einigen Fällen Operationen parallel ausführen können.
Es ist denkbar (auf die Gefahr hin, nicht zum Thema zu gehören?), Dass Sie Python mit Numpy und / oder Scipy verwenden können. Soweit dies möglich war, sind Ihre Berechnungen möglicherweise besser lesbar.
quelle
Da Sie explizit nach Optimierungen auf hoher Ebene gefragt haben, lohnt es sich möglicherweise, verschiedene C ++ - Compiler auszuprobieren. Heutzutage sind Compiler sehr komplexe Optimierungstiere, und CPU-Anbieter implementieren möglicherweise sehr leistungsfähige und spezifische Optimierungen. Bitte beachten Sie, dass einige von ihnen nicht kostenlos sind (es kann jedoch ein kostenloses akademisches Programm geben).
Ich habe gesehen, dass sich Code-Snippets in der Ausführungsgeschwindigkeit um den Faktor 2 unterscheiden, nur durch Ändern des Compilers (natürlich mit vollständigen Optimierungen). Beachten Sie jedoch die Identität der Ausgabe. Eine aggressive Optimierung kann zu unterschiedlichen Ergebnissen führen, was Sie unbedingt vermeiden möchten.
Viel Glück!
quelle