Ist es möglich, diesen Integrationscode so zu optimieren, dass er schneller ausgeführt wird?


9
double trap(double func(double), double b, double a, double N) {
  double j;
  double s;
  double h = (b-a)/(N-1.0); //Width of trapezia

  double func1 = func(a);
  double func2;

  for (s=0,j=a;j<b;j+=h){
    func2 = func(j+h);
    s = s + 0.5*(func1+func2)*h;
    func1 = func2;
  }

  return s;
}

Das Obige ist mein C ++ - Code für eine 1D-numerische Integration (unter Verwendung der erweiterten Trapezregel) func()zwischen den Grenzwerten Verwendung von Trapez .N - 1[ein,b]]N.- -1

Ich mache gerade eine 3D-Integration, bei der dieser Code rekursiv aufgerufen wird. Ich arbeite mit und erhalte gute Ergebnisse.N.=50

Kann jemand außer der weiteren Reduzierung von vorschlagen, wie der obige Code so optimiert werden kann, dass er schneller ausgeführt wird? Oder können Sie sogar eine schnellere Integrationsmethode vorschlagen?N.


5
Dies ist für die Frage nicht wirklich relevant, aber ich würde vorschlagen, bessere Variablennamen zu wählen. Wie trapezoidal_integrationstatt trap, sumoder running_totalanstelle von s(und auch verwenden +=statt s = s +), trapezoid_widthoder dxanstelle von h(oder auch nicht, je nach Ihrer bevorzugten Schreibweise für die Trapezregel), und ändern func1und um func2die Tatsache widerzuspiegeln , dass sie Werte sind, nicht funktioniert. ZB func1-> previous_valueund func2-> current_valueoder so ähnlich.
David Z

Antworten:


5

Mathematisch entspricht Ihr Ausdruck:

ich=h(12f1+f2+f3+...+fn- -1+12fn)+Ö((b- -ein)3fn2)

Das könnten Sie also umsetzen. Wie bereits erwähnt, wird die Zeit wahrscheinlich von der Funktionsbewertung dominiert. Um die gleiche Genauigkeit zu erzielen, können Sie eine bessere Integrationsmethode verwenden, die weniger Funktionsbewertungen erfordert.

Die Gaußsche Quadratur ist heutzutage etwas mehr als ein Spielzeug. Nur nützlich, wenn Sie nur sehr wenige Bewertungen benötigen . Wenn Sie etwas einfach zu implementieren möchten, können Sie die Simpson-Regel verwenden, aber ich würde nicht ohne Grund weiter gehen als bestellen .1/.N.3

Wenn sich die Krümmung der Funktion stark ändert, können Sie eine adaptive Schrittroutine verwenden, die einen größeren Schritt auswählt, wenn die Funktion flach ist, und einen kleineren, genaueren Schritt, wenn die Krümmung höher ist.


Nachdem ich weggegangen bin und auf das Problem zurückgekommen bin, habe ich beschlossen, eine Simpson-Regel zu implementieren. Aber kann ich überprüfen, ob der Fehler in der zusammengesetzten Simpson-Regel tatsächlich proportional zu 1 / (N ^ 4) ist (nicht 1 / (N ^ 3), wie Sie in Ihrer Antwort implizieren)?
user2970116

1
Sie haben Formeln für sowie . Der erste verwendet die Koeffizienten und der zweite . 1 / N 4 5 / 12 , 13 / 12 , 1 , 1 ... 1 , 1 , 13 / 12 , 15 / 12 1 / 3 , 4 / 3 , 2 / 3 , 4 / 3 .. .1/.N.31/.N.45/.12,13/.12,1,1 ... 1,1,13/.12,fünfzehn/.121/.3,4/.3,2/.3,4/.3...
Davidmh

9

Wahrscheinlich ist die Bewertung der Funktionen der zeitaufwändigste Teil dieser Berechnung. Wenn dies der Fall ist, sollten Sie sich darauf konzentrieren, die Geschwindigkeit von func () zu verbessern, anstatt zu versuchen, die Integrationsroutine selbst zu beschleunigen.

Abhängig von den Eigenschaften von func () ist es auch wahrscheinlich, dass Sie mithilfe einer komplexeren Integrationsformel eine genauere Bewertung des Integrals mit weniger Funktionsbewertungen erhalten.


1
Tatsächlich. Wenn Ihre Funktion reibungslos ist, können Sie in der Regel mit weniger als 50 Funktionsbewertungen davonkommen, wenn Sie beispielsweise eine Gauß-4-Quadraturregel in nur 5 Intervallen verwenden.
Wolfgang Bangerth

7

Möglich? Ja. Nützlich? Nein. Es ist unwahrscheinlich, dass die Optimierungen, die ich hier auflisten werde, mehr als einen winzigen Bruchteil eines Prozentunterschieds in der Laufzeit ausmachen. Ein guter Compiler kann dies bereits für Sie tun.

Wie auch immer, wenn Sie Ihre innere Schleife betrachten:

    for (s=0,j=a;j<b;j+=h){
        func2 = func(j+h);
        s = s + 0.5*(func1+func2)*h;
        func1 = func2;
    }

Bei jeder Schleifeniteration führen Sie drei mathematische Operationen aus, die nach außen gebracht werden können: Addieren j + h, Multiplizieren mit 0.5und Multiplizieren mit h. Das erste können Sie beheben, indem Sie Ihre Iteratorvariable bei starten a + h, und das andere, indem Sie die Multiplikationen herausrechnen:

    for (s=0, j=a+h; j<=b; j+=h){
        func2 = func(j);
        s += func1+func2;
        func1 = func2;
    }
    s *= 0.5 * h;

Obwohl ich darauf hinweisen möchte, dass auf diese Weise aufgrund eines Gleitkomma-Rundungsfehlers die letzte Iteration der Schleife übersehen werden kann. (Dies war auch ein Problem in Ihrer ursprünglichen Implementierung.) Um dies zu umgehen, verwenden Sie einen unsigned intoder einen size_tZähler:

    size_t n;
    for (s=0, n=0, j=a+h; n<N; n++, j+=h){
        func2 = func(j);
        s += func1+func2;
        func1 = func2;
    }
    s *= 0.5 * h;

Wie Brians Antwort sagt, ist es besser, Ihre Zeit damit zu verbringen, die Bewertung der Funktion zu optimieren func. Wenn die Genauigkeit dieser Methode ausreicht, werden Sie wahrscheinlich nichts schnelleres finden N. (Sie könnten jedoch einige Tests durchführen, um festzustellen, ob Sie beispielsweise mit Runge-Kutta so weit abgesenkt werden können N, dass die Gesamtintegration weniger Zeit in Anspruch nimmt, ohne die Genauigkeit zu beeinträchtigen.)


4

Es gibt verschiedene Änderungen, die ich empfehlen würde, um die Berechnung zu verbessern:

  • Verwenden Sie aus std::fma()Gründen der Leistung und Genauigkeit eine fusionierte Multiplikationsaddition .
  • Um die Leistung zu verbessern, sollten Sie die Fläche jedes Trapezes mit 0,5 multiplizieren - Sie können dies am Ende einmal tun.
  • Vermeiden Sie das wiederholte Hinzufügen von h, da dies zu Rundungsfehlern führen kann.

Darüber hinaus würde ich aus Gründen der Klarheit einige Änderungen vornehmen:

  • Geben Sie der Funktion einen aussagekräftigeren Namen.
  • Vertauschen Sie die Reihenfolge aund bdie Funktionssignatur.
  • Umbenennen Nn, hdx, jx2, saccumulator.
  • Wechseln Sie nzu einem int.
  • Deklarieren Sie Variablen in einem engeren Bereich.
#include <cmath>

double trapezoidal_integration(double func(double), double a, double b, int n) {
    double dx = (b - a) / (n - 1);   // Width of trapezoids

    double func_x1 = func(a);
    double accumulator = 0;

    for (int i = 1; i <= n; i++) {
        double x2 = a + i * dx;      // Avoid repeated floating-point addition
        double func_x2 = func(x2);
        accumulator = std::fma(func_x1 + func_x2, dx, accumulator); // Fused multiply-add
        func_x1 = func_x2;
    }

    return 0.5 * accumulator;
}

3

Wenn Ihre Funktion ein Polynom ist, das möglicherweise durch eine Funktion gewichtet wird (z. B. ein Gaußscher Wert), können Sie eine genaue Integration in 3D direkt mit einer Kubaturformel durchführen (z. B. http://people.sc.fsu.edu/~jburkardt/c_src/). stroud / stroud.html ) oder mit einem spärlichen Raster (zB http://tasmanian.ornl.gov/ ). Diese Methoden geben einfach eine Reihe von Punkten und Gewichten an, um den Funktionswert mit zu multiplizieren, sodass sie sehr schnell sind. Wenn Ihre Funktion glatt genug ist, um durch Polynome angenähert zu werden, können diese Methoden immer noch eine sehr gute Antwort geben. Die Formeln sind auf die Art der Funktion spezialisiert, die Sie integrieren. Daher kann es einige Zeit dauern, bis Sie die richtige gefunden haben.


3

Wenn Sie versuchen, ein Integral numerisch zu berechnen, versuchen Sie, die gewünschte Präzision mit dem geringstmöglichen Aufwand zu erzielen, oder versuchen Sie alternativ, mit einem festen Aufwand die höchstmögliche Präzision zu erzielen. Sie scheinen sich zu fragen, wie der Code für einen bestimmten Algorithmus so schnell wie möglich ausgeführt werden kann.

Das mag Ihnen einen kleinen Gewinn bringen, aber es wird wenig sein. Es gibt viel effizientere Methoden zur numerischen Integration. Google für "Simpsons Regel", "Runge-Kutta" und "Fehlberg". Sie funktionieren alle ziemlich ähnlich, indem sie einige Werte der Funktion auswerten und ein Vielfaches dieser Werte geschickt addieren, wodurch viel kleinere Fehler mit der gleichen Anzahl von Funktionsauswertungen oder der gleiche Fehler mit einer viel geringeren Anzahl von Auswertungen erzeugt werden.


3

Es gibt viele Möglichkeiten zur Integration, von denen die Trapezregel die einfachste ist.

Wenn Sie überhaupt etwas über die tatsächliche Funktion wissen, die Sie integrieren, können Sie es besser machen, wenn Sie dies ausnutzen. Die Idee ist, die Anzahl der Gitterpunkte innerhalb akzeptabler Fehlerstufen zu minimieren.

Zum Beispiel passt Trapez linear an aufeinanderfolgende Punkte an. Sie könnten eine quadratische Anpassung vornehmen, die bei glatter Kurve besser passt, sodass Sie ein gröberes Gitter verwenden können.

Orbitalsimulationen werden manchmal mit Kegeln durchgeführt, da Bahnen Kegelschnitten sehr ähnlich sind.

In meiner Arbeit integrieren wir Formen, die sich glockenförmigen Kurven annähern, so dass es effektiv ist, sie als solche zu modellieren (die adaptive Gaußsche Quadratur wird in dieser Arbeit als "Goldstandard" angesehen).


1

Wie bereits in anderen Antworten erwähnt, hängt dies stark davon ab, wie teuer Ihre Funktion ist. Die Optimierung Ihres Trapz-Codes lohnt sich nur, wenn es wirklich Ihr Engpass ist. Wenn dies nicht ganz offensichtlich ist, sollten Sie dies überprüfen, indem Sie Ihren Code profilieren (Tools wie Intel V-Tune, Valgrind oder Visual Studio können dies tun).

Ich würde jedoch einen völlig anderen Ansatz vorschlagen: die Monte-Carlo-Integration . Hier approximieren Sie einfach das Integral, indem Sie Ihre Funktion an zufälligen Punkten abtasten und die Ergebnisse hinzufügen. Weitere Informationen finden Sie in diesem PDF zusätzlich zur Wiki-Seite.

Dies funktioniert sehr gut für hochdimensionale Daten, typischerweise viel besser als die Quadraturmethoden, die bei der 1-D-Integration verwendet werden.

Der einfache Fall ist sehr einfach zu implementieren (siehe PDF). Achten Sie jedoch darauf, dass die Standard-Zufallsfunktion in c ++ 98 sowohl hinsichtlich der Leistung als auch der Qualität ziemlich schlecht ist. In c ++ 11 können Sie den Mersenne Twister in verwenden.

Wenn Ihre Funktion in einigen Bereichen stark variiert und in anderen weniger, sollten Sie die Verwendung einer geschichteten Stichprobe in Betracht ziehen. Ich würde empfehlen, die wissenschaftliche Bibliothek der GNU zu verwenden , anstatt Ihre eigene zu schreiben.


1
Ich mache gerade eine 3D-Integration, bei der dieser Code rekursiv aufgerufen wird.

"rekursiv" ist der Schlüssel. Sie durchlaufen entweder einen großen Datensatz und berücksichtigen viele Daten mehr als einmal, oder Sie generieren Ihren Datensatz tatsächlich selbst aus (stückweisen?) Funktionen.

Rekursiv bewertete Integrationen sind lächerlich teuer und lächerlich ungenau, wenn die Potenzen in der Rekursion zunehmen.

Erstellen Sie ein Modell für die Interpolation Ihres Datensatzes und führen Sie eine stückweise symbolische Integration durch. Da dann viele Daten zu Koeffizienten von Basisfunktionen zusammenfallen, wächst die Komplexität für eine tiefere Rekursion eher polynomiell (und normalerweise eher gering) als exponentiell. Und Sie erhalten "genaue" Ergebnisse (Sie müssen immer noch gute Bewertungsschemata finden, um eine angemessene numerische Leistung zu erzielen, aber es sollte immer noch machbar sein, eine bessere als die trapezförmige Integration zu erzielen).

Wenn Sie sich die Fehlerschätzungen für trapezförmige Regeln ansehen, werden Sie feststellen, dass sie mit einer Ableitung der beteiligten Funktionen zusammenhängen, und wenn die Integration / Definition rekursiv erfolgt, haben die Funktionen keine gut verhaltenen Ableitungen .

Wenn Ihr einziges Werkzeug ein Hammer ist, sieht jedes Problem wie ein Nagel aus. Während Sie das Problem in Ihrer Beschreibung kaum ansprechen, habe ich den Verdacht, dass die rekursive Anwendung der Trapezregel eine schlechte Übereinstimmung ist: Sie erhalten eine Explosion sowohl von Ungenauigkeiten als auch von Rechenanforderungen.


1

1/.21/.2

    double trap(double func(double), double b, double a, double N){
double j, s;
double h = (b-a)/(N-1.0); //Width of trapezia

double s = 0;
j = a;
for(i=1; i<N-1; i++){
  j += h;
  s += func(j);
}
s += (func(a)+func(b))/2;

return s*h;
}

1
Bitte begründen Sie Ihre Änderungen und Ihren Code. Ein Codeblock ist für die meisten Menschen ziemlich nutzlos.
Godric Seer

Einverstanden; Bitte erläutern Sie Ihre Antwort.
Geoff Oxberry
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.