Est-il possible d'optimiser ce code d'intégration pour qu'il s'exécute plus rapidement?

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

Ce qui précède est mon code C ++ pour une intégration numérique 1D (en utilisant la règle du trapèze étendu) func()entre les limites utilisant le trapèze .N - 1[une,b]N-1

Je fais actuellement une intégration 3D, où ce code est appelé récursivement. Je travaille avec ce qui me donne des résultats décents.N=50

À part réduire davantage , quelqu'un peut-il suggérer comment optimiser le code ci-dessus pour qu'il s'exécute plus rapidement? Ou, même, peut suggérer une méthode d'intégration plus rapide?N

user2970116
la source
5
Ce n'est pas vraiment pertinent pour la question, mais je suggère de choisir de meilleurs noms de variables. Comme trapezoidal_integrationau lieu de trap, sumou running_totalau lieu de s(et également utiliser à la +=place de s = s +), trapezoid_widthou dxau lieu de h(ou non, selon votre notation préférée pour la règle trapézoïdale), et changez func1et func2pour refléter le fait que ce sont des valeurs, pas des fonctions. Par exemple func1-> previous_valueet func2-> current_value, ou quelque chose comme ça.
David Z

Réponses:

5

Mathématiquement, votre expression équivaut à:

I=h(12f1+f2+f3+...+fn1+12fn)+O((ba)3fn2)

Vous pouvez donc mettre cela en œuvre. Comme il a été dit, le temps est probablement dominé par l'évaluation de la fonction, donc pour obtenir la même précision, vous pouvez utiliser une meilleure méthode d'intégration qui nécessite moins d'évaluations de fonction.

La quadrature gaussienne est, de nos jours, un peu plus qu'un jouet; utile uniquement si vous avez besoin de très peu d'évaluations. Si vous voulez quelque chose de facile à implémenter, vous pouvez utiliser la règle de Simpson, mais je n'irais pas plus loin que commander sans une bonne raison.1/N3

Si la courbure de la fonction change beaucoup, vous pouvez utiliser une routine d'étape adaptative, qui sélectionnerait une étape plus grande lorsque la fonction est plate et une plus petite et plus précise lorsque la courbure est plus élevée.

Davidmh
la source
Après être parti et revenir sur le problème, j'ai décidé de mettre en œuvre une règle de Simpson. Mais puis-je vérifier qu'en fait, l'erreur dans la règle de Simpson composite est proportionnelle à 1 / (N ^ 4) (pas 1 / (N ^ 3) comme vous l'impliquez dans votre réponse)?
user2970116
1
Vous avez des formules pour ainsi que . Le premier utilise les coefficients et le second . 1/N31/N45/12,13/12,1,1...1,1,13/12,15/121/3,4/3,2/3,4/3 ...
Davidmh
9

Il y a de fortes chances que l'évaluation des fonctions soit la partie la plus longue de ce calcul. Si c'est le cas, vous devriez vous concentrer sur l'amélioration de la vitesse de func () plutôt que d'essayer d'accélérer la routine d'intégration elle-même.

Selon les propriétés de func (), il est également probable que vous puissiez obtenir une évaluation plus précise de l'intégrale avec moins d'évaluations de fonctions en utilisant une formule d'intégration plus sophistiquée.

Brian Borchers
la source
1
En effet. Si votre fonction est fluide, vous pouvez généralement vous en tirer avec moins de vos 50 évaluations de fonction si vous utilisez, par exemple, une règle de quadrature de Gauss-4 sur seulement 5 intervalles.
Wolfgang Bangerth
7

Possible? Oui. Utile? Non. Il est peu probable que les optimisations que je vais énumérer ici fassent plus qu'une infime fraction d'un pourcentage de différence dans l'exécution. Un bon compilateur peut déjà les faire pour vous.

Quoi qu'il en soit, en regardant votre boucle intérieure:

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

À chaque itération de boucle, vous effectuez trois opérations mathématiques qui peuvent être apportées à l'extérieur: addition j + h, multiplication par 0.5et multiplication par h. Le premier que vous pouvez corriger en démarrant votre variable d'itérateur sur a + h, et les autres en factorisant les multiplications:

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

Bien que je souligne qu'en faisant cela, en raison d'une erreur d'arrondi en virgule flottante, il est possible de manquer la dernière itération de la boucle. (C'était également un problème dans votre implémentation d'origine.) Pour contourner ce problème, utilisez un unsigned intou un size_tcompteur:

    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;

Comme le dit la réponse de Brian, votre temps est mieux utilisé pour optimiser l'évaluation de la fonction func. Si la précision de cette méthode est suffisante, je doute que vous trouverez quelque chose de plus rapide pour la même chose N. (Bien que vous puissiez exécuter des tests pour voir si, par exemple, Runge-Kutta vous permet de baisser Nsuffisamment pour que l'intégration globale prenne moins de temps sans sacrifier la précision.)

David Z
la source
4

Il y a plusieurs changements que je recommanderais pour améliorer le calcul:

  • Pour des performances et une précision, utilisez std::fma(), qui effectue une multiplication-addition fusionnée .
  • Pour des performances, reportez la multiplication de 0,5 de la surface de chaque trapèze - vous pouvez le faire une fois à la fin.
  • Évitez l'ajout répété de h, qui pourrait accumuler des erreurs d'arrondi.

De plus, j'apporterais plusieurs modifications pour plus de clarté:

  • Donnez à la fonction un nom plus descriptif.
  • Échangez l'ordre de aet bdans la signature de fonction.
  • Renommez Nn, hdx, jx2, saccumulator.
  • Passez nà un int.
  • Déclarez les variables dans une portée plus étroite.
#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;
}
200_success
la source
3

Si votre fonction est un polynôme, éventuellement pondéré par une fonction (par exemple une gaussienne), vous pouvez faire une intégration exacte en 3D directement avec une formule cubature (par exemple http://people.sc.fsu.edu/~jburkardt/c_src/ stroud / stroud.html ) ou avec une grille clairsemée (par exemple http://tasmanian.ornl.gov/ ). Ces méthodes spécifient simplement un ensemble de points et de poids pour multiplier la valeur de la fonction, ils sont donc très rapides. Si votre fonction est suffisamment lisse pour être approximée par des polynômes, ces méthodes peuvent quand même donner une très bonne réponse. Les formules sont spécialisées en fonction du type de fonction que vous intégrez, il peut donc être nécessaire de creuser pour trouver la bonne.

Ronaldo Carpio
la source
3

Lorsque vous essayez de calculer une intégrale numériquement, vous essayez d'obtenir la précision que vous souhaitez avec le moindre effort possible, ou bien, essayez d'obtenir la précision la plus élevée possible avec un effort fixe. Vous semblez demander comment rendre le code d'un algorithme particulier aussi rapide que possible.

Cela peut vous donner un petit gain, mais ce sera peu. Il existe des méthodes beaucoup plus efficaces pour l'intégration numérique. Google pour "la règle de Simpson", "Runge-Kutta" et "Fehlberg". Ils fonctionnent tous de manière assez similaire en évaluant certaines valeurs de la fonction et en ajoutant intelligemment des multiples de ces valeurs, produisant des erreurs beaucoup plus petites avec le même nombre d'évaluations de fonction, ou la même erreur avec un nombre d'évaluations beaucoup plus petit.

gnasher729
la source
3

Il existe de nombreuses façons de faire l'intégration, dont la règle trapézoïdale est la plus simple.

Si vous savez quelque chose sur la fonction réelle que vous intégrez, vous pouvez faire mieux si vous l'exploitez. L'idée est de minimiser le nombre de points de grille dans des niveaux d'erreur acceptables.

Par exemple, trapézoïdal fait un ajustement linéaire à des points consécutifs. Vous pouvez effectuer un ajustement quadratique qui, si la courbe est lisse, conviendrait mieux, ce qui pourrait vous permettre d'utiliser une grille plus grossière.

Les simulations orbitales sont parfois effectuées à l'aide de coniques, car les orbites ressemblent beaucoup à des sections coniques.

Dans mon travail, nous intégrons des formes qui se rapprochent des courbes en forme de cloche, il est donc efficace de les modéliser comme cela ( la quadrature gaussienne adaptative est considérée comme le "gold standard" dans ce travail).

Mike Dunlavey
la source
1

Ainsi, comme cela a été souligné dans d'autres réponses, cela dépend fortement du coût de votre fonction. L'optimisation de votre code trapz ne vaut que si c'est vraiment votre goulot d'étranglement. Si ce n'est pas complètement évident, vous devriez vérifier cela en profilant votre code (des outils comme Intels V-tune, Valgrind ou Visual Studio peuvent le faire).

Je proposerais cependant une approche complètement différente: l' intégration de Monte Carlo . Ici, vous approximez simplement l'intégrale en échantillonnant votre fonction à des points aléatoires en ajoutant les résultats. Voir ce pdf en plus de la page wiki pour plus de détails.

Cela fonctionne extrêmement bien pour les données de grande dimension, généralement beaucoup mieux que les méthodes de quadrature utilisées dans l'intégration 1-d.

Le cas simple est très facile à implémenter (voir le pdf), faites juste attention à ce que la fonction aléatoire standard en c ++ 98 soit assez mauvaise en termes de performances et de qualité. En c ++ 11, vous pouvez utiliser le Mersenne Twister dans.

Si votre fonction présente beaucoup de variations dans certains domaines et moins dans d'autres, envisagez d'utiliser l'échantillonnage stratifié. Je recommanderais cependant d'utiliser la bibliothèque scientifique GNU , plutôt que d'écrire la vôtre.

LKlevin
la source
1
Je fais actuellement une intégration 3D, où ce code est appelé récursivement.

"récursivement" est la clé. Vous parcourez un grand ensemble de données et envisagez plusieurs fois plusieurs données, ou bien vous générez vous-même votre ensemble de données à partir de fonctions (par morceaux?).

Les intégrations évaluées de manière récursive seront ridiculement chères et ridiculement imprécises à mesure que les puissances augmentent en récursivité.

Créez un modèle pour interpoler votre ensemble de données et effectuez une intégration symbolique par morceaux. Étant donné que de nombreuses données s'effondrent ensuite en coefficients de fonctions de base, la complexité d'une récursion plus profonde croît de manière polynomiale (et généralement plutôt de faibles puissances) plutôt qu'exponentiellement. Et vous obtenez des résultats "exacts" (vous devez toujours trouver de bons schémas d'évaluation pour obtenir des performances numériques raisonnables, mais il devrait toujours être plutôt possible de faire mieux que l'intégration trapézoïdale).

Si vous regardez les estimations d'erreur pour les règles trapézoïdales, vous constaterez qu'elles sont liées à une dérivée des fonctions impliquées, et si l'intégration / la définition est récursive, les fonctions n'auront pas tendance à avoir des dérivées bien comportées. .

Si votre seul outil est un marteau, chaque problème ressemble à un clou. Alors que vous abordez à peine le problème dans votre description, j'ai le soupçon que l'application récursive de la règle trapézoïdale est une mauvaise correspondance: vous obtenez une explosion d'exigences d'inexactitude et de calcul.

David
la source
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;
}
Aksakal presque sûrement binaire
la source
1
Veuillez motiver vos modifications et votre code. Un bloc de code est assez inutile pour la plupart des gens.
Godric Seer
D'accord; veuillez expliquer votre réponse.
Geoff Oxberry