È possibile ottimizzare questo codice di integrazione in modo che funzioni più velocemente?


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

Quanto sopra è il mio codice C ++ per un'integrazione numerica 1D (usando la regola del trapezio estesa) func()tra i limiti usando il trapezia .N - 1[a,b]N1

In realtà sto facendo un'integrazione 3D, in cui questo codice viene chiamato in modo ricorsivo. Lavoro con dandomi risultati decenti.N=50

Oltre a ridurre ulteriormente , qualcuno è in grado di suggerire come ottimizzare il codice sopra in modo che funzioni più velocemente? O, addirittura, può suggerire un metodo di integrazione più veloce?N


5
Questo non è veramente rilevante per la domanda, ma suggerirei di scegliere nomi di variabili migliori. Come trapezoidal_integrationinvece di trap, sumo running_totalinvece di s(e anche usare +=invece di s = s +), trapezoid_widtho dxinvece di h(o no, a seconda della notazione preferita per la regola trapezoidale), e cambiare func1e func2riflettere il fatto che sono valori, non funzioni. Ad esempio func1-> previous_valuee func2-> current_value, o qualcosa del genere.
David Z,

Risposte:


5

Matematicamente, la tua espressione equivale a:

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

Quindi potresti implementarlo. Come è stato detto, il tempo è probabilmente dominato dalla valutazione della funzione, quindi per ottenere la stessa precisione, è possibile utilizzare un metodo di integrazione migliore che richiede meno valutazioni della funzione.

La quadratura gaussiana è, ai giorni nostri, un po 'più di un giocattolo; utile solo se avete bisogno di molto poche valutazioni. Se vuoi qualcosa di facile da implementare, puoi usare la regola di Simpson, ma non andrei oltre l'ordine senza una buona ragione.1/N3

Se la curvatura della funzione cambia molto, è possibile utilizzare una routine di passi adattativa, che selezionerebbe un passo più grande quando la funzione è piatta e uno più piccolo e più accurato quando la curvatura è più alta.


Dopo essere andato via e tornare al problema, ho deciso di attuare una regola di Simpson. Ma posso verificare che in realtà l'errore nella regola composita di Simpson sia proporzionale a 1 / (N ^ 4) (non 1 / (N ^ 3) come implica nella risposta)?
user2970116

1
Hai formule per e . Il primo utilizza i coefficienti e il secondo . 1/N31/N45/12,13/12,1,1...1,1,13/12,15/121/3,4/3,2/3,4/3...
Davidmh,

9

È probabile che la valutazione delle funzioni sia la parte che richiede più tempo di questo calcolo. In tal caso, dovresti concentrarti sul miglioramento della velocità di func () piuttosto che cercare di accelerare la routine di integrazione stessa.

A seconda delle proprietà di func (), è anche probabile che tu possa ottenere una valutazione più precisa dell'integrale con meno valutazioni delle funzioni utilizzando una formula di integrazione più sofisticata.


1
Infatti. Se la tua funzione è fluida, in genere puoi cavartela con meno delle tue 50 valutazioni se hai usato, diciamo, una regola di quadratura di Gauss-4 a soli 5 intervalli.
Wolfgang Bangerth,

7

Possibile? Sì. Utile? No. È improbabile che le ottimizzazioni che sto per elencare qui facciano più di una piccola frazione della differenza percentuale nel tempo di esecuzione. Un buon compilatore potrebbe già fare queste cose per te.

Ad ogni modo, guardando il tuo ciclo interiore:

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

Ad ogni iterazione di loop esegui tre operazioni matematiche che possono essere portate all'esterno: aggiunta j + h, moltiplicazione per 0.5e moltiplicazione per h. Il primo che puoi correggere avviando la tua variabile iteratore su a + h, e gli altri prendendo in considerazione le moltiplicazioni:

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

Anche se vorrei sottolineare che, facendo questo, a causa dell'errore di arrotondamento in virgola mobile è possibile perdere l'ultima iterazione del ciclo. (Anche questo era un problema nella tua implementazione originale.) Per aggirare il problema, usa un unsigned into un size_tcontatore:

    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;

Come dice la risposta di Brian, il tuo tempo è meglio speso per ottimizzare la valutazione della funzione func. Se l'accuratezza di questo metodo è sufficiente, dubito che troverai qualcosa di più veloce per lo stesso N. (Sebbene sia possibile eseguire alcuni test per vedere se, ad esempio, Runge-Kutta ti consente di abbassare Nabbastanza da rendere l'integrazione complessiva che richiede meno tempo senza sacrificare la precisione.)


4

Ci sono diverse modifiche che consiglierei di migliorare il calcolo:

  • Per prestazioni e precisione, utilizzare std::fma(), che esegue un'aggiunta multipla fusa .
  • Per prestazioni, rimanda moltiplicando l'area di ciascun trapezio per 0,5 - puoi farlo una volta alla fine.
  • Evitare l'aggiunta ripetuta di h, che potrebbe accumulare errori di arrotondamento.

Inoltre, vorrei apportare diverse modifiche per chiarezza:

  • Assegna alla funzione un nome più descrittivo.
  • Scambia l'ordine di ae bnella firma della funzione.
  • Rinomina Nn, hdx, jx2, saccumulator.
  • Passare na un int.
  • Dichiarare le variabili in un ambito più ristretto.
#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

Se la tua funzione è un polinomio, eventualmente ponderata da una funzione (ad esempio un gaussiano), puoi fare un'esatta integrazione in 3d direttamente con una formula cubatura (ad esempio http://people.sc.fsu.edu/~jburkardt/c_src/ stroud / stroud.html ) o con una griglia sparsa (ad esempio http://tasmanian.ornl.gov/ ). Questi metodi specificano semplicemente un insieme di punti e pesi per moltiplicare il valore della funzione, quindi sono molto veloci. Se la tua funzione è abbastanza fluida da essere approssimata dai polinomi, questi metodi possono comunque dare un'ottima risposta. Le formule sono specializzate nel tipo di funzione che stai integrando, quindi potrebbe essere necessario scavare per trovare quella giusta.


3

Quando si tenta di calcolare un integrale numericamente, si tenta di ottenere la precisione desiderata con il minimo sforzo possibile o, in alternativa, si tenta di ottenere la massima precisione possibile con uno sforzo fisso. Sembra che tu chieda come eseguire il codice per un determinato algoritmo il più velocemente possibile.

Questo potrebbe darti un piccolo guadagno, ma sarà poco. Esistono metodi molto più efficienti per l'integrazione numerica. Google per "La regola di Simpson", "Runge-Kutta" e "Fehlberg". Funzionano tutti abbastanza simili valutando alcuni valori della funzione e aggiungendo abilmente multipli di quel valore, producendo errori molto più piccoli con lo stesso numero di valutazioni della funzione o lo stesso errore con un numero molto più piccolo di valutazioni.


3

Esistono molti modi per fare l'integrazione, di cui la regola trapezoidale è la più semplice.

Se sai qualcosa della funzione che stai integrando, puoi fare di meglio se la sfrutti. L'idea è di ridurre al minimo il numero di punti della griglia entro livelli accettabili di errore.

Ad esempio, il trapezoidale sta adattando linearmente punti consecutivi. Si potrebbe fare un adattamento quadratico, che se la curva fosse liscia si adatterebbe meglio, il che potrebbe consentire di utilizzare una griglia più grossolana.

Le simulazioni orbitali a volte vengono eseguite utilizzando le coniche, poiché le orbite sono molto simili alle sezioni coniche.

Nel mio lavoro, stiamo integrando forme che si avvicinano alle curve a forma di campana, quindi è efficace modellarle in questo modo ( la quadratura gaussiana adattiva è considerata lo "standard d'oro" in questo lavoro).


1

Quindi, come è stato sottolineato in altre risposte, questo dipende fortemente da quanto è costosa la tua funzione. L'ottimizzazione del codice trapz ne vale la pena solo se è davvero il collo di bottiglia. Se non è del tutto ovvio, dovresti verificarlo profilando il tuo codice (strumenti come Intels V-tune, Valgrind o Visual Studio possono farlo).

Vorrei tuttavia suggerire un approccio completamente diverso: l' integrazione di Monte Carlo . Qui puoi semplicemente approssimare l'integrale campionando la tua funzione in punti casuali aggiungendo i risultati. Vedi questo pdf oltre alla pagina wiki per i dettagli.

Funziona molto bene con dati ad alta dimensione, in genere molto meglio dei metodi di quadratura usati nell'integrazione 1-d.

Il semplice caso è molto facile da implementare (vedi il pdf), fai solo attenzione che la funzione casuale standard in c ++ 98 sia piuttosto male sia in termini di prestazioni che di qualità. In c ++ 11, puoi usare Mersenne Twister in.

Se la tua funzione presenta molte variazioni in alcune aree e meno in altre, considera l'utilizzo del campionamento stratificato. Consiglierei di usare la libreria scientifica GNU , piuttosto che scriverne una tua.


1
In realtà sto facendo un'integrazione 3D, in cui questo codice è chiamato ricorsivamente.

"ricorsivamente" è la chiave. O stai attraversando un set di dati di grandi dimensioni e stai prendendo in considerazione molti dati più di una volta, oppure stai effettivamente generando il tuo set di dati da funzioni (a tratti?).

Le integrazioni valutate in modo ricorsivo saranno ridicolmente costose e ridicolmente imprecise man mano che i poteri aumentano nella ricorsione.

Crea un modello per l'interpolazione del tuo set di dati ed esegui un'integrazione simbolica a tratti. Poiché molti dati stanno quindi collassando in coefficienti delle funzioni di base, la complessità per una ricorsione più profonda cresce polinomialmente (e di solito con potenze piuttosto basse) anziché esponenzialmente. E ottieni risultati "esatti" (devi ancora trovare buoni schemi di valutazione per ottenere prestazioni numeriche ragionevoli, ma dovrebbe essere comunque fattibile migliorare l'integrazione trapezoidale).

Se dai un'occhiata alle stime di errore per le regole trapezoidali, scoprirai che sono correlate ad alcune derivate delle funzioni coinvolte e se l'integrazione / definizione viene eseguita in modo ricorsivo, le funzioni non tenderanno ad avere derivati ​​ben educati .

Se il tuo unico strumento è un martello, ogni problema sembra un chiodo. Mentre tocchi a malapena il problema nella tua descrizione, ho il sospetto che l'applicazione della regola trapezoidale ricorsivamente sia una cattiva corrispondenza: si ottiene un'esplosione di inesattezze e requisiti computazionali.


1

il codice originale valuta la funzione in ciascun N punti, quindi aggiunge i valori e moltiplica la somma per la dimensione del passo. l'unico trucco è che i valori all'inizio e alla fine vengono aggiunti con peso , mentre tutti i punti all'interno vengono aggiunti a peso pieno. in realtà, vengono anche aggiunti con peso ma due volte. invece di aggiungerli due volte, aggiungerli solo una volta a pieno peso. fattorizzare la moltiplicazione per la dimensione del gradino all'esterno del loop. questo è tutto ciò che si può fare per accelerarlo, davvero.1 / 21/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
Si prega di fornire un ragionamento per le modifiche e il codice. Un blocco di codice è abbastanza inutile per la maggior parte delle persone.
Godric Seer,

Concordato; per favore, spiega la tua risposta.
Geoff Oxberry,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.