Come posso migliorare le prestazioni tramite un approccio di alto livello durante l'implementazione di lunghe equazioni in C ++


92

Sto sviluppando alcune simulazioni ingegneristiche. Ciò comporta l'implementazione di alcune lunghe equazioni come questa equazione per calcolare lo stress in un materiale simile alla gomma:

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;

Uso Maple per generare il codice C ++ per evitare errori (e risparmiare tempo con noiose algebra). Poiché questo codice viene eseguito migliaia (se non milioni) di volte, le prestazioni sono un problema. Purtroppo la matematica finora si semplifica solo; le lunghe equazioni sono inevitabili.

Quale approccio posso adottare per ottimizzare questa implementazione? Sto cercando strategie di alto livello che dovrei applicare durante l'implementazione di tali equazioni, non necessariamente ottimizzazioni specifiche per l'esempio mostrato sopra.

Sto compilando usando g ++ con --enable-optimize=-O3.

Aggiornare:

So che ci sono molte espressioni ripetute, sto usando il presupposto che il compilatore le gestisca; i miei test finora suggeriscono di sì.

l1, l2, l3, mu, a, K sono tutti numeri reali positivi (non zero).

Ho sostituito l1*l2*l3con una variabile equivalente: J. Ciò ha contribuito a migliorare le prestazioni.

Sostituirlo pow(x, 0.1e1/0.3e1)con è cbrt(x)stato un buon suggerimento.

Questo verrà eseguito su CPU, nel prossimo futuro probabilmente funzionerà meglio su GPU, ma per ora tale opzione non è disponibile.


32
Bene, la prima cosa che viene in mente (a meno che il compilatore non lo ottimizzi da solo) è sostituire tutti quelli pow(l1 * l2 * l3, -0.1e1 / 0.3e1)con una variabile ... Tuttavia, è necessario eseguire un benchmark del codice per essere sicuri che funzioni velocemente o lentamente.
SingerOfTheFall

6
Formattare anche il codice per renderlo più leggibile - può aiutare a identificare le possibilità di miglioramento.
Ed Heal

26
Perché tutti i voti negativi e i voti per chiudere? Per quelli di voi che non amano la programmazione numerica o scientifica, guardate altre domande. Questa è una buona domanda che ben si adatta a questo sito. Il sito scicomp è ancora beta; la migrazione non è una buona opzione. Il sito di revisione del codice non ha abbastanza occhi da sciomp. Ciò che l'OP ha fatto accade abbastanza spesso nell'informatica scientifica: costruire un problema in un programma di matematica simbolico, chiedere al programma di generare codice e non toccare il risultato perché il codice generato è un tale casino.
David Hammen

6
@DavidHammen il sito Code Review non ha abbastanza occhi da fantascientifico - sembra un problema con l'uovo e la gallina e una mentalità che non aiuta CR a ottenere altri di questi occhi. Lo stesso vale per l'idea di rifiutare il sito beta di scicomp perché è beta : se tutti la pensassero in questo modo, l'unico sito a crescere sarebbe Stack Overflow.
Mathieu Guindon

13
Questa domanda è in discussione su meta qui
NathanOliver

Risposte:


88

Modifica riepilogo

  • La mia risposta originale notava semplicemente che il codice conteneva molti calcoli replicati e che molte delle potenze coinvolgevano fattori di 1/3. Ad esempio, pow(x, 0.1e1/0.3e1)è lo stesso di cbrt(x).
  • La mia seconda modifica era semplicemente sbagliata e la mia terza estrapolata su questa inesattezza. Questo è ciò che fa paura alle persone di cambiare i risultati simili a quelli di un oracolo da programmi di matematica simbolici che iniziano con la lettera "M". Ho cancellato (cioè barrato ) quelle modifiche e le ho spinte alla fine dell'attuale revisione di questa risposta. Tuttavia, non li ho eliminati. Sono umano. È facile per noi sbagliare.
  • La quarta modifica sviluppato un'espressione molto compatta che rappresenta correttamente l'espressione contorta nell'interrogazione IF i parametri l1, l2e l3sono numeri reali positivi e se aè un numero reale diverso da zero. (Dobbiamo ancora sentire dal PO in merito alla natura specifica di questi coefficienti. Data la natura del problema, queste sono ipotesi ragionevoli.)
  • Questa modifica tenta di rispondere al problema generico di come semplificare queste espressioni.

Cominciando dall'inizio

Uso Maple per generare il codice C ++ per evitare errori.

Maple e Mathematica a volte perdono l'ovvio. Ancora più importante, gli utenti di Maple e Mathematica a volte commettono errori. Sostituire "spesso", o forse anche "quasi sempre", al posto di "a volte è probabilmente più vicino al segno.

Avresti potuto aiutare Maple a semplificare quell'espressione raccontandogli i parametri in questione. Nell'esempio a portata di mano, sospetto che l1, l2e l3siano numeri reali positivi e che asia un numero reale diverso da zero. Se è così, diglielo. Quei programmi di matematica simbolici in genere presumono che le quantità a portata di mano siano complesse. La limitazione del dominio consente al programma di formulare ipotesi che non sono valide nei numeri complessi.


Come semplificare quei grandi pasticci dai programmi di matematica simbolica (questa modifica)

I programmi di matematica simbolica in genere forniscono la capacità di fornire informazioni sui vari parametri. Usa questa capacità, soprattutto se il tuo problema riguarda la divisione o l'elevazione a potenza. Nell'esempio a portata di mano, si potrebbe avere contribuito a Maple semplificare questa espressione dicendo che essa l1, l2e l3sono numeri reali positivi e che aè un numero reale diverso da zero. Se è così, diglielo. Quei programmi di matematica simbolici in genere presumono che le quantità a portata di mano siano complesse. La limitazione del dominio consente al programma di fare ipotesi come a x b x = (ab) x . Questo è solo se ae bsono numeri reali positivi e se xè reale. Non è valido nei numeri complessi.

In definitiva, quei programmi matematici simbolici seguono algoritmi. Aiutalo. Prova a giocare con l'espansione, la raccolta e la semplificazione prima di generare il codice. In questo caso, avresti potuto raccogliere quei termini che coinvolgono un fattore di mue quelli che coinvolgono un fattore di K. Ridurre un'espressione alla sua "forma più semplice" rimane un po 'un'arte.

Quando ottieni un brutto pasticcio di codice generato, non accettarlo come una verità che non devi toccare. Prova a semplificarlo da solo. Guarda cosa aveva il programma di matematica simbolica prima di generare il codice. Guarda come ho ridotto la tua espressione a qualcosa di molto più semplice e molto più veloce, e come la risposta di Walter ha portato la mia diversi passi avanti. Non esiste una ricetta magica. Se ci fosse stata una ricetta magica, Maple l'avrebbe applicata e avrebbe dato la risposta data da Walter.


Sulla domanda specifica

Stai facendo molte addizioni e sottrazioni in quel calcolo. Puoi finire nei guai se hai termini che quasi si annullano a vicenda. Stai sprecando molta CPU se hai un termine che domina sugli altri.

Successivamente, stai sprecando molta CPU eseguendo calcoli ripetuti. A meno che tu non abbia abilitato -ffast-math, che consente al compilatore di infrangere alcune delle regole del punto mobile IEEE, il compilatore non semplificherà (in effetti, non deve) quell'espressione per te. Invece farà esattamente quello che gli hai detto di fare. Come minimo, dovresti calcolare l1 * l2 * l3prima di calcolare quel casino.

Infine, stai facendo molte chiamate a pow, il che è estremamente lento. Notare che molte di queste chiamate sono nel formato (l1 * l2 * l3) (1/3) . Molte di queste chiamate a powpotrebbero essere eseguite con una singola chiamata a std::cbrt:

l123 = l1 * l2 * l3;
l123_pow_1_3 = std::cbrt(l123);
l123_pow_4_3 = l123 * l123_pow_1_3;

Con questo,

  • X * pow(l1 * l2 * l3, 0.1e1 / 0.3e1)diventa X * l123_pow_1_3.
  • X * pow(l1 * l2 * l3, -0.1e1 / 0.3e1)diventa X / l123_pow_1_3.
  • X * pow(l1 * l2 * l3, 0.4e1 / 0.3e1)diventa X * l123_pow_4_3.
  • X * pow(l1 * l2 * l3, -0.4e1 / 0.3e1)diventa X / l123_pow_4_3.


Maple ha mancato l'ovvio.
Ad esempio, c'è un modo molto più semplice per scrivere

(pow(l1 * l2 * l3, -0.1e1 / 0.3e1) - l1 * l2 * l3 * pow(l1 * l2 * l3, -0.4e1 / 0.3e1) / 0.3e1)

Supponendo che l1, l2e l3siano numeri reali piuttosto che complessi, e che si debba estrarre la radice cubica reale (piuttosto che la radice complessa principale), quanto sopra si riduce a

2.0/(3.0 * pow(l1 * l2 * l3, 1.0/3.0))

o

2.0/(3.0 * l123_pow_1_3)

Usando cbrt_l123invece di l123_pow_1_3, l'espressione sgradevole nella domanda si riduce a

l123 = l1 * l2 * l3; 
cbrt_l123 = cbrt(l123);
T = 
  mu/(3.0*l123)*(  pow(l1/cbrt_l123,a)*(2.0*N1-N2-N3)
                 + pow(l2/cbrt_l123,a)*(2.0*N2-N3-N1)
                 + pow(l3/cbrt_l123,a)*(2.0*N3-N1-N2))
 +K*(l123-1.0)*(N1+N2+N3);

Ricontrolla sempre, ma semplifica sempre.


Ecco alcuni dei miei passi per arrivare a quanto sopra:

// Step 0: Trim all whitespace.
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;

// Step 1:
//   l1*l2*l3 -> l123
//   0.1e1 -> 1.0
//   0.4e1 -> 4.0
//   0.3e1 -> 3
l123 = l1 * l2 * l3;
T=(mu*(pow(l1*pow(l123,-1.0/3),a)*a*(pow(l123,-1.0/3)-l123*pow(l123,-4.0/3)/3)*pow(l123,1.0/3)/l1-pow(l2*pow(l123,-1.0/3),a)*a/l1/3-pow(l3*pow(l123,-1.0/3),a)*a/l1/3)/a+K*(l123-1.0)*l2*l3)*N1/l2/l3+(mu*(-pow(l1*pow(l123,-1.0/3),a)*a/l2/3+pow(l2*pow(l123,-1.0/3),a)*a*(pow(l123,-1.0/3)-l123*pow(l123,-4.0/3)/3)*pow(l123,1.0/3)/l2-pow(l3*pow(l123,-1.0/3),a)*a/l2/3)/a+K*(l123-1.0)*l1*l3)*N2/l1/l3+(mu*(-pow(l1*pow(l123,-1.0/3),a)*a/l3/3-pow(l2*pow(l123,-1.0/3),a)*a/l3/3+pow(l3*pow(l123,-1.0/3),a)*a*(pow(l123,-1.0/3)-l123*pow(l123,-4.0/3)/3)*pow(l123,1.0/3)/l3)/a+K*(l123-1.0)*l1*l2)*N3/l1/l2;

// Step 2:
//   pow(l123,1.0/3) -> cbrt_l123
//   l123*pow(l123,-4.0/3) -> pow(l123,-1.0/3)
//   (pow(l123,-1.0/3)-pow(l123,-1.0/3)/3) -> 2.0/(3.0*cbrt_l123)
//   *pow(l123,-1.0/3) -> /cbrt_l123
l123 = l1 * l2 * l3;
cbrt_l123 = cbrt(l123);
T=(mu*(pow(l1/cbrt_l123,a)*a*2.0/(3.0*cbrt_l123)*cbrt_l123/l1-pow(l2/cbrt_l123,a)*a/l1/3-pow(l3/cbrt_l123,a)*a/l1/3)/a+K*(l123-1.0)*l2*l3)*N1/l2/l3+(mu*(-pow(l1/cbrt_l123,a)*a/l2/3+pow(l2/cbrt_l123,a)*a*2.0/(3.0*cbrt_l123)*cbrt_l123/l2-pow(l3/cbrt_l123,a)*a/l2/3)/a+K*(l123-1.0)*l1*l3)*N2/l1/l3+(mu*(-pow(l1/cbrt_l123,a)*a/l3/3-pow(l2/cbrt_l123,a)*a/l3/3+pow(l3/cbrt_l123,a)*a*2.0/(3.0*cbrt_l123)*cbrt_l123/l3)/a+K*(l123-1.0)*l1*l2)*N3/l1/l2;

// Step 3:
//   Whitespace is nice.
l123 = l1 * l2 * l3;
cbrt_l123 = cbrt(l123);
T =
  (mu*( pow(l1/cbrt_l123,a)*a*2.0/(3.0*cbrt_l123)*cbrt_l123/l1
       -pow(l2/cbrt_l123,a)*a/l1/3
       -pow(l3/cbrt_l123,a)*a/l1/3)/a
   +K*(l123-1.0)*l2*l3)*N1/l2/l3
 +(mu*(-pow(l1/cbrt_l123,a)*a/l2/3
       +pow(l2/cbrt_l123,a)*a*2.0/(3.0*cbrt_l123)*cbrt_l123/l2
       -pow(l3/cbrt_l123,a)*a/l2/3)/a
   +K*(l123-1.0)*l1*l3)*N2/l1/l3
 +(mu*(-pow(l1/cbrt_l123,a)*a/l3/3
       -pow(l2/cbrt_l123,a)*a/l3/3
       +pow(l3/cbrt_l123,a)*a*2.0/(3.0*cbrt_l123)*cbrt_l123/l3)/a
   +K*(l123-1.0)*l1*l2)*N3/l1/l2;

// Step 4:
//   Eliminate the 'a' in (term1*a + term2*a + term3*a)/a
//   Expand (mu_term + K_term)*something to mu_term*something + K_term*something
l123 = l1 * l2 * l3;
cbrt_l123 = cbrt(l123);
T =
  (mu*( pow(l1/cbrt_l123,a)*2.0/(3.0*cbrt_l123)*cbrt_l123/l1
       -pow(l2/cbrt_l123,a)/l1/3
       -pow(l3/cbrt_l123,a)/l1/3))*N1/l2/l3
 +K*(l123-1.0)*l2*l3*N1/l2/l3
 +(mu*(-pow(l1/cbrt_l123,a)/l2/3
       +pow(l2/cbrt_l123,a)*2.0/(3.0*cbrt_l123)*cbrt_l123/l2
       -pow(l3/cbrt_l123,a)/l2/3))*N2/l1/l3
 +K*(l123-1.0)*l1*l3*N2/l1/l3
 +(mu*(-pow(l1/cbrt_l123,a)/l3/3
       -pow(l2/cbrt_l123,a)/l3/3
       +pow(l3/cbrt_l123,a)*2.0/(3.0*cbrt_l123)*cbrt_l123/l3))*N3/l1/l2
 +K*(l123-1.0)*l1*l2*N3/l1/l2;

// Step 5:
//   Rearrange
//   Reduce l2*l3*N1/l2/l3 to N1 (and similar)
//   Reduce 2.0/(3.0*cbrt_l123)*cbrt_l123/l1 to 2.0/3.0/l1 (and similar)
l123 = l1 * l2 * l3;
cbrt_l123 = cbrt(l123);
T =
  (mu*( pow(l1/cbrt_l123,a)*2.0/3.0/l1
       -pow(l2/cbrt_l123,a)/l1/3
       -pow(l3/cbrt_l123,a)/l1/3))*N1/l2/l3
 +(mu*(-pow(l1/cbrt_l123,a)/l2/3
       +pow(l2/cbrt_l123,a)*2.0/3.0/l2
       -pow(l3/cbrt_l123,a)/l2/3))*N2/l1/l3
 +(mu*(-pow(l1/cbrt_l123,a)/l3/3
       -pow(l2/cbrt_l123,a)/l3/3
       +pow(l3/cbrt_l123,a)*2.0/3.0/l3))*N3/l1/l2
 +K*(l123-1.0)*N1
 +K*(l123-1.0)*N2
 +K*(l123-1.0)*N3;

// Step 6:
//   Factor out mu and K*(l123-1.0)
l123 = l1 * l2 * l3;
cbrt_l123 = cbrt(l123);
T =
  mu*(  ( pow(l1/cbrt_l123,a)*2.0/3.0/l1
         -pow(l2/cbrt_l123,a)/l1/3
         -pow(l3/cbrt_l123,a)/l1/3)*N1/l2/l3
      + (-pow(l1/cbrt_l123,a)/l2/3
         +pow(l2/cbrt_l123,a)*2.0/3.0/l2
         -pow(l3/cbrt_l123,a)/l2/3)*N2/l1/l3
      + (-pow(l1/cbrt_l123,a)/l3/3
         -pow(l2/cbrt_l123,a)/l3/3
         +pow(l3/cbrt_l123,a)*2.0/3.0/l3)*N3/l1/l2)
 +K*(l123-1.0)*(N1+N2+N3);

// Step 7:
//   Expand
l123 = l1 * l2 * l3;
cbrt_l123 = cbrt(l123);
T =
  mu*( pow(l1/cbrt_l123,a)*2.0/3.0/l1*N1/l2/l3
      -pow(l2/cbrt_l123,a)/l1/3*N1/l2/l3
      -pow(l3/cbrt_l123,a)/l1/3*N1/l2/l3
      -pow(l1/cbrt_l123,a)/l2/3*N2/l1/l3
      +pow(l2/cbrt_l123,a)*2.0/3.0/l2*N2/l1/l3
      -pow(l3/cbrt_l123,a)/l2/3*N2/l1/l3
      -pow(l1/cbrt_l123,a)/l3/3*N3/l1/l2
      -pow(l2/cbrt_l123,a)/l3/3*N3/l1/l2
      +pow(l3/cbrt_l123,a)*2.0/3.0/l3*N3/l1/l2)
 +K*(l123-1.0)*(N1+N2+N3);

// Step 8:
//   Simplify.
l123 = l1 * l2 * l3;
cbrt_l123 = cbrt(l123);
T =
  mu/(3.0*l123)*(  pow(l1/cbrt_l123,a)*(2.0*N1-N2-N3)
                 + pow(l2/cbrt_l123,a)*(2.0*N2-N3-N1)
                 + pow(l3/cbrt_l123,a)*(2.0*N3-N1-N2))
 +K*(l123-1.0)*(N1+N2+N3);


Risposta sbagliata, mantenuta intenzionalmente per umiltà

Notare che questo è colpito. È sbagliato.

Aggiornare

Maple ha mancato l'ovvio. Ad esempio, c'è un modo molto più semplice per scrivere

(pow (l1 * l2 * l3, -0.1e1 / 0.3e1) - l1 * l2 * l3 * pow (l1 * l2 * l3, -0.4e1 / 0.3e1) / 0.3e1)

Supponendo che l1, l2e l3siano numeri reali piuttosto che complessi e che si debba estrarre la radice cubica reale (piuttosto che la radice complessa principale), quanto sopra si riduce a zero. Questo calcolo dello zero viene ripetuto molte volte.

Secondo aggiornamento

Se ho fatto il diritto di matematica (non v'è alcuna garanzia che ho fatto il diritto di matematica), la brutta espressione nella questione si riduce a

l123 = l1 * l2 * l3; 
cbrt_l123_inv = 1.0 / cbrt(l123);
nasty_expression =
    K * (l123 - 1.0) * (N1 + N2 + N3) 
    - (  pow(l1 * cbrt_l123_inv, a) * (N2 + N3) 
       + pow(l2 * cbrt_l123_inv, a) * (N1 + N3) 
       + pow(l3 * cbrt_l123_inv, a) * (N1 + N2)) * mu / (3.0*l123);

Quanto sopra presuppone che l1, l2e l3siano numeri reali positivi.


2
Bene, l'eliminazione del CSE dovrebbe funzionare, indipendentemente dalla semantica rilassata (e OP indicato nei commenti lo fa). Anche se ovviamente, se è importante (misurato), dovrebbe essere ispezionato (assemblaggio generato). I tuoi punti sui termini dominanti, sulle semplificazioni delle formule mancate, sulle funzioni specializzate migliori e sui pericoli della cancellazione sono molto buoni.
Deduplicator

3
@Deduplicator - Non con virgola mobile. A meno che non si abilitino ottimizzazioni matematiche non sicure (ad esempio, specificando -ffast-mathcon gcc o clang), il compilatore non può fare affidamento pow(x,-1.0/3.0)sull'essere uguale a x*pow(x,-4.0/3.0). Quest'ultimo potrebbe underflow mentre il primo potrebbe no. Per essere conforme allo standard in virgola mobile, il compilatore non deve ottimizzare il calcolo a zero.
David Hammen

Bene, quelli sono molto più ambiziosi di qualsiasi cosa intendessi.
Deduplicator

1
@Deduplicator: Come ho commentato un'altra risposta : hai bisogno -fno-math-errnodi powchiamate identiche da g ++ a CSE . (A meno che forse non può dimostrare che pow non avrà bisogno di impostare errno?)
Peter Cordes

1
@Lefti - Prendi molto alla risposta di Walter. È molto più veloce. C'è un potenziale problema con tutte queste risposte, che è la cancellazione numerica. Supponendo che i tuoi N1, N2e N3non siano negativi, uno 2*N_i-(N_j+N_k)sarà negativo, uno sarà positivo e l'altro sarà da qualche parte nel mezzo. Ciò può facilmente causare problemi di cancellazione numerica.
David Hammen

32

La prima cosa da notare è che powè molto costoso, quindi dovresti sbarazzartene il più possibile. Scorrendo l'espressione vedo molte ripetizioni di pow(l1 * l2 * l3, -0.1e1 / 0.3e1)e pow(l1 * l2 * l3, -0.4e1 / 0.3e1). Quindi mi aspetto un grande guadagno dal pre-calcolo di quelli:

 const double c1 = pow(l1 * l2 * l3, -0.1e1 / 0.3e1);
const double c2 = boost::math::pow<4>(c1);

dove sto usando la funzione boost pow .

Inoltre, ne hai altri powcon esponente a. Se aè Integer e noto al momento del compilatore, puoi anche sostituirli con boost::math::pow<a>(...)per ottenere ulteriori prestazioni. Suggerirei anche di sostituire termini come a / l1 / 0.3e1con a / (l1 * 0.3e1)poiché la moltiplicazione è più veloce della divisione.

Infine, se usi g ++ puoi usare il -ffast-mathflag che permette all'ottimizzatore di essere più aggressivo nella trasformazione delle equazioni. Leggi cosa fa effettivamente questa bandiera , poiché ha effetti collaterali.


5
Nel nostro codice, l'utilizzo di -ffast-mathlead il codice diventa instabile o fornisce risposte sbagliate. Abbiamo un problema simile con i compilatori Intel e dobbiamo usare l' -fp-model preciseopzione, altrimenti il ​​codice esplode o dà le risposte sbagliate. Quindi -ffast-mathpotrebbe accelerarlo, ma consiglierei di procedere con molta cautela con questa opzione, oltre agli effetti collaterali elencati nella tua domanda collegata.
tpg2114

2
@ tpg2114: Secondo i miei test, è sufficiente-fno-math-errno che g ++ sia in grado di powestrarre chiamate identiche da un loop. Questa è la parte meno "pericolosa" di -ffast-math, per la maggior parte del codice.
Peter Cordes

1
@PeterCordes Questi sono risultati interessanti! Abbiamo anche avuto problemi con l' pow essere estremamente lenti e abbiamo finito per utilizzare l' dlsymhack menzionato nei commenti per ottenere notevoli aumenti delle prestazioni quando avremmo potuto effettivamente farlo con un po 'meno precisione.
tpg2114

GCC non capirebbe che pow è una funzione pura? Probabilmente è una conoscenza incorporata.
usr

6
@usr: Questo è solo il punto, credo. nonpow è una funzione pura, secondo lo standard, perché in alcune circostanze dovrebbe essere impostata . L'impostazione di flag come causa di non impostare (quindi violando lo standard), ma poi è una funzione pura e può essere ottimizzata come tale. errno-fno-math-errnoerrno
Nate Eldredge

20

Woah, che espressione infernale. Creare l'espressione con Maple in realtà è stata una scelta non ottimale qui. Il risultato è semplicemente illeggibile.

  1. ha scelto nomi di variabili pronunciati (non l1, l2, l3, ma ad esempio altezza, larghezza, profondità, se questo è ciò che significano). Quindi è più facile per te capire il tuo codice.
  2. calcolare i sottotermini, che si utilizzano più volte, in anticipo e memorizzare i risultati in variabili con nomi pronunciati.
  3. Hai detto che l'espressione viene valutata molte volte. Immagino che solo pochi parametri variano nel ciclo più interno. Calcola tutti i sottotermini invarianti prima di quel ciclo. Ripeti per il secondo ciclo interno e così via fino a quando tutti gli invarianti sono fuori dal ciclo.

Teoricamente il compilatore dovrebbe essere in grado di fare tutto questo per te, ma a volte non può, ad esempio quando l'annidamento del ciclo si estende su più funzioni in diverse unità di compilazione. Ad ogni modo, questo ti darà un codice leggibile, comprensibile e gestibile molto meglio.


8
"Il compilatore dovrebbe farlo, ma a volte no", è la chiave qui. oltre alla leggibilità, ovviamente.
Javier

3
Se il compilatore non è tenuto a fare qualcosa, supponendo che sia quasi sempre sbagliato.
edmz

4
Scegli nuovamente nomi di variabili pronunciati - Molte volte questa bella regola non si applica quando fai matematica. Quando guardo il codice che dovrebbe implementare un algoritmo in una rivista scientifica, preferirei di gran lunga vedere i simboli nel codice essere esattamente quelli usati nella rivista. In genere, ciò significa nomi estremamente brevi, possibilmente con un pedice.
David Hammen

8
"Il risultato è semplicemente illeggibile": perché è un problema? Non ti importerebbe che l'output del linguaggio di alto livello da un generatore di lexer o parser fosse "illeggibile" (dagli umani). Ciò che conta qui è che l' input al generatore di codice (Maple) sia leggibile e verificabile. La cosa da non fare è modificare manualmente il codice generato, se vuoi essere sicuro che sia privo di errori.
alephzero

3
@DavidHammen: Bene, in quel caso, quelli di una lettera sono i "nomi pronunciati". Ad esempio, quando si esegue la geometria in un sistema di coordinate cartesiane bidimensionali xe nony sono variabili a lettera singola prive di significato, sono parole intere con una definizione precisa e un significato ben compreso.
Jörg W Mittag

17

La risposta di David Hammen è buona, ma tutt'altro che ottimale. Continuiamo con la sua ultima espressione (nel momento in cui scrivo questo)

auto l123 = l1 * l2 * l3;
auto cbrt_l123 = cbrt(l123);
T = mu/(3.0*l123)*(  pow(l1/cbrt_l123,a)*(2.0*N1-N2-N3)
                   + pow(l2/cbrt_l123,a)*(2.0*N2-N3-N1)
                   + pow(l3/cbrt_l123,a)*(2.0*N3-N1-N2))
  + K*(l123-1.0)*(N1+N2+N3);

che può essere ulteriormente ottimizzato. In particolare, possiamo evitare la chiamata a cbrt()e una delle chiamate a pow()se sfruttando alcune identità matematiche. Facciamolo di nuovo passo dopo passo.

// step 1 eliminate cbrt() by taking the exponent into pow()
auto l123 = l1 * l2 * l3;
auto athird = 0.33333333333333333 * a; // avoid division
T = mu/(3.0*l123)*(  (N1+N1-N2-N3)*pow(l1*l1/(l2*l3),athird)
                   + (N2+N2-N3-N1)*pow(l2*l2/(l1*l3),athird)
                   + (N3+N3-N1-N2)*pow(l3*l3/(l1*l2),athird))
  + K*(l123-1.0)*(N1+N2+N3);

Nota che ho anche ottimizzato 2.0*N1per N1+N1ecc. Successivamente, possiamo fare solo due chiamate a pow().

// step 2  eliminate one call to pow
auto l123 = l1 * l2 * l3;
auto athird = 0.33333333333333333 * a;
auto pow_l1l2_athird = pow(l1/l2,athird);
auto pow_l1l3_athird = pow(l1/l3,athird);
auto pow_l2l3_athird = pow_l1l3_athird/pow_l1l2_athird;
T = mu/(3.0*l123)*(  (N1+N1-N2-N3)* pow_l1l2_athird*pow_l1l3_athird
                   + (N2+N2-N3-N1)* pow_l2l3_athird/pow_l1l2_athird
                   + (N3+N3-N1-N2)/(pow_l1l3_athird*pow_l2l3_athird))
  + K*(l123-1.0)*(N1+N2+N3);

Poiché le chiamate a pow()sono di gran lunga l'operazione più costosa qui, vale la pena ridurle il più possibile (la successiva operazione costosa è stata la chiamata a cbrt(), che abbiamo eliminato).

Se per caso aè intero, le chiamate a powpotrebbero essere ottimizzate per chiamate a cbrt(più potenze intere), oppure se athirdè mezzo intero, possiamo usare sqrt(più potenze intere). Inoltre, se per caso l1==l2o l1==l3o l2==l3una o entrambe le chiamate a powpossono essere eliminate. Quindi, vale la pena considerarli come casi speciali se tali possibilità esistono realisticamente.


@gnat Apprezzo il tuo editing (ho pensato di farlo io stesso), ma l'avrei trovato più giusto, se la risposta di David si collegasse anche a questa. Perché non modifichi anche la risposta di David in modo simile?
Walter

1
L'ho modificato solo perché ti ho visto menzionarlo esplicitamente; Ho riletto la risposta di David e non sono riuscito a trovare un riferimento alla tua risposta. Cerco di evitare modifiche in cui non è chiaro al 100% che le cose che aggiungo corrispondono alle intenzioni dell'autore
gnat

1
@ Walter - La mia risposta ora si collega alla tua.
David Hammen

1
Di certo non sono stato io. Ho votato positivamente la tua risposta qualche giorno fa. Ho anche ricevuto un voto negativo di flyby casuale sulla mia risposta. A volte succedono cose.
David Hammen

1
Tu ed io abbiamo ricevuto un misero voto negativo ciascuno. Guarda tutti i voti negativi sulla domanda! Ad oggi, la domanda ha ricevuto 16 voti negativi. Ha anche ricevuto 80 voti positivi che hanno più che compensato tutti quei voti negativi.
David Hammen

12
  1. Quante è "molte molte"?
  2. Quanto tempo ci vuole?
  3. Do TUTTI parametri cambiano tra il ricalcolo di questa formula? O puoi memorizzare nella cache alcuni valori pre-calcolati?
  4. Ho tentato una semplificazione manuale di quella formula, vorrei sapere se salva qualcosa?

    C1 = -0.1e1 / 0.3e1;
    C2 =  0.1e1 / 0.3e1;
    C3 = -0.4e1 / 0.3e1;
    
    X0 = l1 * l2 * l3;
    X1 = pow(X0, C1);
    X2 = pow(X0, C2);
    X3 = pow(X0, C3);
    X4 = pow(l1 * X1, a);
    X5 = pow(l2 * X1, a);
    X6 = pow(l3 * X1, a);
    X7 = a / 0.3e1;
    X8 = X3 / 0.3e1;
    X9 = mu / a;
    XA = X0 - 0.1e1;
    XB = K * XA;
    XC = X1 - X0 * X8;
    XD = a * XC * X2;
    
    XE = X4 * X7;
    XF = X5 * X7;
    XG = X6 * X7;
    
    T = (X9 * ( X4 * XD - XF - XG) / l1 + XB * l2 * l3) * N1 / l2 / l3 
      + (X9 * (-XE + X5 * XD - XG) / l2 + XB * l1 * l3) * N2 / l1 / l3 
      + (X9 * (-XE - XF + X6 * XD) / l3 + XB * l1 * l2) * N3 / l1 / l2;

[AGGIUNTO] Ho lavorato ancora un po 'sull'ultima formula di tre righe e sono arrivato a questa bellezza:

T = X9 / X0 * (
      (X4 * XD - XF - XG) * N1 + 
      (X5 * XD - XE - XG) * N2 + 
      (X5 * XD - XE - XF) * N3)
  + XB * (N1 + N2 + N3)

Fammi mostrare il mio lavoro, passo dopo passo:

T = (X9 * (X4 * XD - XF - XG) / l1 + XB * l2 * l3) * N1 / l2 / l3 
  + (X9 * (X5 * XD - XE - XG) / l2 + XB * l1 * l3) * N2 / l1 / l3 
  + (X9 * (X5 * XD - XE - XF) / l3 + XB * l1 * l2) * N3 / l1 / l2;


T = (X9 * (X4 * XD - XF - XG) / l1 + XB * l2 * l3) * N1 / (l2 * l3) 
  + (X9 * (X5 * XD - XE - XG) / l2 + XB * l1 * l3) * N2 / (l1 * l3) 
  + (X9 * (X5 * XD - XE - XF) / l3 + XB * l1 * l2) * N3 / (l1 * l2);

T = (X9 * (X4 * XD - XF - XG) + XB * l1 * l2 * l3) * N1 / (l1 * l2 * l3) 
  + (X9 * (X5 * XD - XE - XG) + XB * l1 * l2 * l3) * N2 / (l1 * l2 * l3) 
  + (X9 * (X5 * XD - XE - XF) + XB * l1 * l2 * l3) * N3 / (l1 * l2 * l3);

T = (X9 * (X4 * XD - XF - XG) + XB * X0) * N1 / X0 
  + (X9 * (X5 * XD - XE - XG) + XB * X0) * N2 / X0 
  + (X9 * (X5 * XD - XE - XF) + XB * X0) * N3 / X0;

T = X9 * (X4 * XD - XF - XG) * N1 / X0 + XB * N1 
  + X9 * (X5 * XD - XE - XG) * N2 / X0 + XB * N2
  + X9 * (X5 * XD - XE - XF) * N3 / X0 + XB * N3;


T = X9 * (X4 * XD - XF - XG) * N1 / X0 
  + X9 * (X5 * XD - XE - XG) * N2 / X0
  + X9 * (X5 * XD - XE - XF) * N3 / X0
  + XB * (N1 + N2 + N3)

2
Notevole, eh? :) FORTRAN, IIRC, è stato progettato per calcoli efficienti di formule ("FOR" sta per formula).
Vlad Feinstein

La maggior parte dei codici F77 che ho visto sembrava così (ad esempio, BLAS e NR). Molto contento che Fortran 90-> 2008 esista :)
Kyle Kanos

Sì. Se stai traducendo una formula, quale modo migliore di FORmulaTRANslation?
Brian Drummond

1
La tua "ottimizzazione" attacca il posto sbagliato. I bit costosi sono le chiamate a std::pow(), di cui hai ancora 6, 3 volte più del necessario. In altre parole, il tuo codice è 3 volte più lento del possibile.
Walter

7

Questo può essere un po 'conciso, ma in realtà ho trovato una buona velocità per i polinomi (interpolazione di funzioni energetiche) usando Horner Form, che fondamentalmente riscrive ax^3 + bx^2 + cx + dcome d + x(c + x(b + x(a))). Ciò eviterà molte chiamate ripetute pow()e ti impedirà di fare cose stupide come chiamare separatamente pow(x,6)e pow(x,7)invece di fare solo x*pow(x,6).

Questo non è direttamente applicabile al tuo problema attuale, ma se hai polinomi di ordine elevato con potenze intere può essere d'aiuto. Potrebbe essere necessario fare attenzione alla stabilità numerica e ai problemi di overflow poiché l'ordine delle operazioni è importante per questo (anche se in generale penso che la forma di Horner aiuti in questo, poiché x^20e di xsolito sono molti ordini di grandezza a parte).

Anche come consiglio pratico, se non l'hai già fatto, prova prima a semplificare l'espressione in acero. Probabilmente puoi convincerlo a eseguire la maggior parte delle comuni eliminazioni di sottoespressioni per te. Non so quanto influisca in particolare sul generatore di codice in quel programma, ma so che in Mathematica fare un FullSimplify prima di generare il codice può comportare un'enorme differenza.


La forma di Horner è piuttosto standard per la codifica dei polinomi e questo non ha alcuna rilevanza per la domanda.
Walter

1
Questo può essere vero dato il suo esempio, ma noterai che ha detto "equazioni di questo tipo". Ho pensato che la risposta sarebbe stata utile se il poster avesse dei polinomi nel suo sistema. Ho notato in particolare che i generatori di codice per i programmi CAS come Mathematica e Maple tendono a NON darti la forma di Horner a meno che tu non lo richieda specificatamente; sono predefiniti nel modo in cui normalmente si scrive un polinomio da umano.
neocpp

3

Sembra che tu abbia molte operazioni ripetute in corso.

pow(l1 * l2 * l3, -0.1e1 / 0.3e1)
pow(l1 * l2 * l3, -0.4e1 / 0.3e1)

Potresti pre-calcolarli in modo da non chiamare ripetutamente la powfunzione che può essere costosa.

Potresti anche pre-calutare

l1 * l2 * l3

mentre usi ripetutamente quel termine.


6
Scommetto che l'ottimizzatore lo fa già per te ... anche se rende almeno il codice più leggibile.
Karoly Horvath

L'ho fatto, ma non ha accelerato affatto le cose. Ho pensato che fosse perché l'ottimizzazione del compilatore se ne stava già occupando.

la memorizzazione di l1 * l2 * l3 accelera le cose, non sono sicuro del perché con l'ottimizzazione del compilatore

perché a volte il compilatore non può eseguire alcune ottimizzazioni o le trova in conflitto con altre opzioni.
Javier

1
In effetti, il compilatore non deve eseguire tali ottimizzazioni a meno che non -ffast-mathsia abilitato e, come notato in un commento di @ tpg2114, tale ottimizzazione può creare risultati estremamente instabili.
David Hammen

0

Se hai una scheda grafica Nvidia CUDA, potresti considerare di scaricare i calcoli sulla scheda grafica, che a sua volta è più adatta per calcoli computazionalmente complicati.

https://developer.nvidia.com/how-to-cuda-c-cpp

In caso contrario, potresti prendere in considerazione più thread per i calcoli.


10
Questa risposta è ortogonale alla domanda in esame. Sebbene le GPU abbiano moltissimi processori, sono piuttosto lente rispetto all'FPU integrato con la CPU. L'esecuzione di un singolo calcolo seriale con una GPU è una grande perdita. La CPU deve riempire la pipeline fino alla GPU, attendere che la GPU lenta esegua quella singola operazione, quindi scaricare il risultato. Sebbene le GPU siano assolutamente fantastiche quando il problema in questione è massicciamente parallelizzabile, sono assolutamente atroci quando si tratta di eseguire attività seriali.
David Hammen

1
Nella domanda originale: "Poiché questo codice viene eseguito molte volte, le prestazioni sono un problema.". Questo è uno in più di "molti". L'operatore può inviare i calcoli in modo filettato.
user3791372

0

Per caso, potresti fornire il calcolo simbolicamente. Se ci sono operazioni vettoriali, potresti davvero voler investigare usando blas o lapack che in alcuni casi possono eseguire operazioni in parallelo.

È concepibile (a rischio di essere fuori tema?) Che potresti essere in grado di usare python con numpy e / o scipy. Nella misura in cui è stato possibile, i tuoi calcoli potrebbero essere più leggibili.


0

Dato che hai chiesto esplicitamente delle ottimizzazioni di alto livello, potrebbe valere la pena provare diversi compilatori C ++. Al giorno d'oggi, i compilatori sono bestie di ottimizzazione molto complesse ei fornitori di CPU potrebbero implementare ottimizzazioni molto potenti e specifiche. Ma tieni presente che alcuni di essi non sono gratuiti (ma potrebbe esserci un programma accademico gratuito).

  • La raccolta di compilatori GNU è gratuita, flessibile e disponibile su molte architetture
  • I compilatori Intel sono molto veloci, molto costosi e possono anche produrre buoni risultati per le architetture AMD (credo che esista un programma accademico)
  • I compilatori Clang sono veloci, gratuiti e potrebbero produrre risultati simili a GCC (alcune persone dicono che sono più veloci, migliori, ma questo potrebbe differire per ogni caso di applicazione, suggerisco di fare le tue esperienze)
  • PGI (Portland Group) non è gratuito come i compilatori Intel.
  • I compilatori PathScale potrebbero fornire buoni risultati su architetture AMD

Ho visto frammenti di codice differire nella velocità di esecuzione del fattore 2, solo cambiando il compilatore (con ottimizzazioni complete ovviamente). Ma essere consapevoli di controllare l'identità dell'output. Un'ottimizzazione aggressiva potrebbe portare a risultati diversi, che è qualcosa che si desidera assolutamente evitare.

In bocca al lupo!

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.