Rozwijam kilka symulacji inżynierskich. Obejmuje to zastosowanie kilku długich równań, takich jak to równanie, w celu obliczenia naprężenia w materiale podobnym do gumy:
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;
Używam Maple do generowania kodu C ++, aby uniknąć błędów (i zaoszczędzić czas dzięki żmudnej algebrze). Ponieważ ten kod jest wykonywany tysiące (jeśli nie miliony) razy, wydajność jest problemem. Niestety matematyka na razie się tylko upraszcza; długie równania są nieuniknione.
Jakie podejście mogę zastosować, aby zoptymalizować tę implementację? Szukam strategii wysokiego poziomu, które powinienem zastosować przy implementacji takich równań, niekoniecznie konkretnych optymalizacji dla przykładu pokazanego powyżej.
Kompiluję za pomocą g ++ z --enable-optimize=-O3
.
Aktualizacja:
Wiem, że jest wiele powtarzających się wyrażeń, przyjmuję założenie, że kompilator je obsłuży; moje dotychczasowe testy sugerują, że tak.
l1, l2, l3, mu, a, K
są dodatnimi liczbami rzeczywistymi (nie zerem).
Wymieniłem l1*l2*l3
z zmiennej równoważne: J
. Pomogło to poprawić wydajność.
Wymiana pow(x, 0.1e1/0.3e1)
z cbrt(x)
była dobra sugestia.
Będzie działać na procesorach. W najbliższej przyszłości prawdopodobnie działałoby to lepiej na procesorach graficznych, ale na razie ta opcja nie jest dostępna.
pow(l1 * l2 * l3, -0.1e1 / 0.3e1)
zmienną ... Musisz jednak przetestować swój kod, aby upewnić się, czy działa szybko, czy wolno.Odpowiedzi:
Edytuj podsumowanie
pow(x, 0.1e1/0.3e1)
to to samo, cocbrt(x)
.Wykreśliłem) te zmiany i umieściłem je na dole aktualnej wersji tej odpowiedzi. Jednak ich nie usunąłem. Jestem człowiekiem. Łatwo nam popełnić błąd.l1
,l2
il3
są dodatnimi liczbami rzeczywistymi, a jeślia
jest niezerowe liczby rzeczywiste. (Nie otrzymaliśmy jeszcze informacji z PO na temat specyfiki tych współczynników. Biorąc pod uwagę naturę problemu, są to rozsądne założenia).Po pierwsze
Maple i Mathematica czasami pomijają oczywistość. Co ważniejsze, użytkownicy Maple i Mathematica czasami popełniają błędy. Zastępowanie „często”, a może nawet „prawie zawsze” zamiast „czasami jest prawdopodobnie bliżej celu.
Mogłeś pomóc Maple uprościć to wyrażenie, mówiąc mu o danych parametrach. W omawianym przykładzie podejrzewam, że
l1
,l2
il3
są dodatnimi liczbami rzeczywistymi, a toa
jest niezerową liczbą rzeczywistą. Jeśli tak jest, powiedz to. Te symboliczne programy matematyczne zazwyczaj zakładają, że dostępne ilości są złożone. Ograniczenie domeny pozwala programowi przyjmować założenia, które nie są poprawne w liczbach zespolonych.Jak uprościć te duże bałagania z symbolicznych programów matematycznych (ta edycja)
Programy do matematyki symbolicznej zazwyczaj zapewniają możliwość dostarczania informacji o różnych parametrach. Użyj tej zdolności, szczególnie jeśli twój problem obejmuje dzielenie lub potęgowanie. Na przykład pod ręką, można pomogły Klon uprościć że ekspresja informując go, że
l1
,l2
il3
są liczbami rzeczywistymi dodatnimi i żea
jest niezerowe liczby rzeczywiste. Jeśli tak jest, powiedz to. Te symboliczne programy matematyczne zazwyczaj zakładają, że dostępne ilości są złożone. Ograniczenie domeny pozwala programowi przyjmować takie założenia, jak a x b x = (ab) x . To jest tylko wtedy, gdya
ib
są dodatnimi liczbami rzeczywistymi, a jeślix
jest prawdziwe. Nie dotyczy liczb zespolonych.Ostatecznie te symboliczne programy matematyczne są zgodne z algorytmami. Pomóż temu. Spróbuj bawić się rozwijaniem, gromadzeniem i upraszczaniem, zanim wygenerujesz kod. W tym przypadku mógłbyś zebrać te terminy, które zawierają czynnik,
mu
a te z czynnikiemK
. Sprowadzenie wypowiedzi do „najprostszej formy” pozostaje sztuką.Kiedy otrzymujesz brzydki bałagan wygenerowanego kodu, nie akceptuj tego jako prawdy, której nie możesz dotykać. Spróbuj sam to uprościć. Spójrz, co miał symboliczny program matematyczny, zanim wygenerował kod. Spójrz, jak zredukowałem twój wyraz twarzy do czegoś znacznie prostszego i znacznie szybszego i jak odpowiedź Waltera posunęła moją o kilka kroków dalej. Nie ma magicznego przepisu. Gdyby istniał magiczny przepis, Maple zastosowałby go i udzielił odpowiedzi, której udzielił Walter.
O konkretnym pytaniu
Robisz dużo dodawania i odejmowania w tych obliczeniach. Możesz wpaść w poważne kłopoty, jeśli masz warunki, które prawie się znoszą. Marnujesz dużo procesora, jeśli masz jeden termin, który dominuje nad innymi.
Następnie marnujesz dużo procesora, wykonując powtarzające się obliczenia. O ile nie włączyłeś tej opcji
-ffast-math
, która pozwala kompilatorowi złamać niektóre reguły zmiennoprzecinkowe IEEE, kompilator nie będzie (w rzeczywistości nie może) upraszczać tego wyrażenia za Ciebie. Zamiast tego zrobi dokładnie to, co mu kazałeś. Jako minimum powinieneś obliczyćl1 * l2 * l3
przed obliczeniem tego bałaganu.Wreszcie wykonujesz wiele połączeń
pow
, co jest bardzo powolne. Zauważ, że kilka z tych wywołań ma postać (l1 * l2 * l3) (1/3) . Wiele z tych wywołańpow
można wykonać jednym wywołaniemstd::cbrt
:l123 = l1 * l2 * l3; l123_pow_1_3 = std::cbrt(l123); l123_pow_4_3 = l123 * l123_pow_1_3;
Z tym,
X * pow(l1 * l2 * l3, 0.1e1 / 0.3e1)
staje sięX * l123_pow_1_3
.X * pow(l1 * l2 * l3, -0.1e1 / 0.3e1)
staje sięX / l123_pow_1_3
.X * pow(l1 * l2 * l3, 0.4e1 / 0.3e1)
staje sięX * l123_pow_4_3
.X * pow(l1 * l2 * l3, -0.4e1 / 0.3e1)
staje sięX / l123_pow_4_3
.Maple przegapił oczywistość.
Na przykład istnieje znacznie łatwiejszy sposób pisania
(pow(l1 * l2 * l3, -0.1e1 / 0.3e1) - l1 * l2 * l3 * pow(l1 * l2 * l3, -0.4e1 / 0.3e1) / 0.3e1)
Zakładając, że
l1
,l2
il3
są prawdziwe zamiast liczb zespolonych, a korzeniem rzeczywistego modułu (zamiast zasady kompleks korzeniowego) do ekstrakcji, powyżej redukuje się do2.0/(3.0 * pow(l1 * l2 * l3, 1.0/3.0))
lub
2.0/(3.0 * l123_pow_1_3)
Używanie
cbrt_l123
zamiastl123_pow_1_3
paskudnego wyrażenia w pytaniu sprowadza się dol123 = 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);
Zawsze sprawdzaj dokładnie, ale zawsze upraszczaj.
Oto niektóre z moich kroków prowadzących do powyższego:
// 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);
Zła odpowiedź, celowo zachowana dla pokory
Zauważ, że to jest dotknięte. To jest źle.
AktualizacjaMaple przegapił oczywistość. Na przykład istnieje znacznie łatwiejszy sposób pisania
Przy założeniu
l1
,l2
il3
są prawdziwe zamiast liczb zespolonych, a rzeczywista modułu głównego (zamiast zasady kompleks korzeniowego) do ekstrakcji, powyżej redukuje się do zera. To obliczenie zera powtarza się wielokrotnie.Druga aktualizacja
Jeśli poprawnie wykonałem matematykę (nie ma gwarancji, że poprawnie wykonałem obliczenia), nieprzyjemne wyrażenie w pytaniu sprowadza się do
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);
Powyższe zakłada, żel1
,l2
il3
są dodatnimi liczbami rzeczywistymi.źródło
-ffast-math
pomocą gcc lub clang), kompilator nie może polegać na tym,pow(x,-1.0/3.0)
że jest równyx*pow(x,-4.0/3.0)
. Ta ostatnia może być niedostateczna, podczas gdy pierwsza może nie. Aby zachować zgodność ze standardem zmiennoprzecinkowym, kompilator nie może optymalizować tego obliczenia do zera.-fno-math-errno
identycznychpow
wywołań g ++ do CSE . (Chyba że może to udowodnić, że pow nie będzie musiał ustawiać errno?)N1
, że twojeN2
iN3
są nieujemne, jedna z nich2*N_i-(N_j+N_k)
będzie ujemna, jedna będzie dodatnia, a druga będzie gdzieś pomiędzy. Może to łatwo spowodować problemy z anulowaniem numerycznym.Pierwszą rzeczą, na którą należy zwrócić uwagę, jest to, że
pow
jest to naprawdę drogie, więc powinieneś się tego pozbyć jak najwięcej. Przeglądając wyrażenie, widzę wiele powtórzeńpow(l1 * l2 * l3, -0.1e1 / 0.3e1)
ipow(l1 * l2 * l3, -0.4e1 / 0.3e1)
. Spodziewałbym się więc dużego zysku z wstępnego obliczania tych:const double c1 = pow(l1 * l2 * l3, -0.1e1 / 0.3e1); const double c2 = boost::math::pow<4>(c1);
gdzie używam funkcji doładowania pow .
Co więcej, masz więcej
pow
z wykładnikiema
. Jeślia
jest to liczba całkowita i znana w czasie kompilacji, możesz również zastąpić je,boost::math::pow<a>(...)
aby uzyskać dalszą wydajność. Proponuję również zastąpić wyrażenia takie jaka / l1 / 0.3e1
z,a / (l1 * 0.3e1)
ponieważ mnożenie jest szybsze niż dzielenie.Wreszcie, jeśli używasz g ++, możesz użyć
-ffast-math
flagi, która pozwala optymalizatorowi na bardziej agresywne przekształcanie równań. Przeczytaj o tym, co faktycznie robi ta flaga , ponieważ ma ona jednak skutki uboczne.źródło
-ffast-math
prowadzi do niestabilności lub podania błędnych odpowiedzi. Mamy podobny problem z kompilatorami Intela i musimy skorzystać z tej-fp-model precise
opcji, w przeciwnym razie kod albo wybuchnie, albo poda błędne odpowiedzi. Więc-ffast-math
może to przyspieszyć, ale zalecałbym bardzo ostrożne postępowanie z tą opcją, oprócz skutków ubocznych wymienionych w powiązanym pytaniu.-fno-math-errno
g ++, aby móc podnosić identyczne wywołaniapow
z pętli. To najmniej „niebezpieczna” część -ffast-math dla większości kodu.pow
byciem bardzo powolnym i skończyło się na tym, że wykorzystaliśmydlsym
hack wspomniany w komentarzach, aby uzyskać znaczny wzrost wydajności, podczas gdy faktycznie mogliśmy to zrobić z nieco mniejszą precyzją.pow
to nie czysta funkcja, zgodnie z normą, ponieważ ma zestawerrno
w pewnych okolicznościach. Ustawienie flag, takich jak-fno-math-errno
powoduje, że nie jest ustawianaerrno
(naruszając tym samym standard), ale wtedy jest to czysta funkcja i jako taka może być zoptymalizowana.Woah, co za piekielna ekspresja. Utworzenie wyrażenia za pomocą Maple w rzeczywistości było tutaj nieoptymalnym wyborem. Wynik jest po prostu nieczytelny.
Teoretycznie kompilator powinien być w stanie zrobić to wszystko za Ciebie, ale czasami nie może - np. Gdy zagnieżdżanie pętli obejmuje wiele funkcji w różnych jednostkach kompilacji. W każdym razie, da ci to znacznie bardziej czytelny, zrozumiały i łatwiejszy w utrzymaniu kod.
źródło
x
i niey
są to bezsensowne zmienne jednoliterowe, są to całe słowa z precyzyjną definicją i dobrze i szeroko rozumianym znaczeniem.Odpowiedź Davida Hammena jest dobra, ale wciąż daleka od optymalnej. Kontynuujmy jego ostatnie wyrażenie (w momencie pisania tego)
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);
które można dalej optymalizować. W szczególności możemy uniknąć wywołania
cbrt()
i jednego z wezwań,pow()
jeśli wykorzystujemy pewne tożsamości matematyczne. Zróbmy to ponownie krok po kroku.// 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);
Zauważ, że zoptymalizowałem również
2.0*N1
doN1+N1
itp. Następnie możemy zrobić tylko dwa wywołaniapow()
.// 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);
Ponieważ wezwania do
pow()
są tutaj zdecydowanie najbardziej kosztowną operacją, warto je maksymalnie ograniczyć (następną kosztowną operacją było wezwanie docbrt()
, które wyeliminowaliśmy).Jeśli przypadkiem
a
jest to liczba całkowita, wywołaniapow
mogą być zoptymalizowane pod kątem wywołańcbrt
(plus potęgi całkowite), lub jeśliathird
jest to pół-liczba całkowita, możemy użyćsqrt
(plus potęgi całkowite). Ponadto, jeśli przypadkieml1==l2
albol1==l3
czyl2==l3
jedno lub oba połączenia, abypow
można wyeliminować. Warto więc traktować te przypadki jako szczególne, jeśli realnie istnieją takie szanse.źródło
Próbowałem ręcznie uprościć tę formułę, czy chciałbym wiedzieć, czy coś zapisuje?
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;
[DODANE] Pracowałem trochę więcej nad ostatnią formułą trzech wierszy i sprowadziłem to do tego piękna:
Pokażę moją pracę krok po kroku:
źródło
std::pow()
, których wciąż masz 6, 3 razy więcej niż to konieczne. Innymi słowy, twój kod jest 3 razy wolniejszy niż to możliwe.Może to być trochę lakoniczne, ale w rzeczywistości znalazłem dobre przyspieszenie dla wielomianów (interpolacja funkcji energii) przy użyciu formy Hornera, która w zasadzie przepisuje
ax^3 + bx^2 + cx + d
jakod + x(c + x(b + x(a)))
. Pozwoli to uniknąć wielu powtarzających się połączeńpow()
i powstrzyma Cię przed robieniem głupich rzeczy, takich jak oddzielne dzwonieniepow(x,6)
ipow(x,7)
zamiast po prostu robićx*pow(x,6)
.Nie ma to bezpośredniego zastosowania do twojego obecnego problemu, ale jeśli masz wielomiany wysokiego rzędu z mocami całkowitymi, może to pomóc. Być może będziesz musiał uważać na problemy ze stabilnością liczbową i przepełnieniem, ponieważ kolejność operacji jest do tego ważna (chociaż ogólnie uważam, że Forma Horner pomaga w tym, ponieważ
x^20
ix
są zwykle o wiele rzędów wielkości).Również jako praktyczna wskazówka, jeśli jeszcze tego nie zrobiłeś, spróbuj najpierw uprościć wyrażenie w klonie. Prawdopodobnie możesz go zmusić do wykonywania większości typowych czynności eliminacji podwyrażeń za Ciebie. Nie wiem, jak bardzo wpływa to na generator kodu w tym programie, ale wiem, że w Mathematica wykonanie FullSimplify przed wygenerowaniem kodu może spowodować ogromną różnicę.
źródło
Wygląda na to, że wykonywanych jest wiele powtarzających się operacji.
pow(l1 * l2 * l3, -0.1e1 / 0.3e1) pow(l1 * l2 * l3, -0.4e1 / 0.3e1)
Możesz je wstępnie obliczyć, aby nie wywoływać wielokrotnie
pow
funkcji, która może być kosztowna.Możesz również wstępnie obliczyć
gdy używasz tego terminu wielokrotnie.
źródło
-ffast-math
jest włączony, a jak zauważono w komentarzu @ tpg2114, optymalizacja ta może powodować wyjątkowo niestabilne wyniki.Jeśli masz kartę graficzną Nvidia CUDA, możesz rozważyć przeniesienie obliczeń na kartę graficzną - która sama w sobie jest bardziej odpowiednia dla skomplikowanych obliczeniowo obliczeń.
https://developer.nvidia.com/how-to-cuda-c-cpp
Jeśli nie, możesz rozważyć wiele wątków do obliczeń.
źródło
Czy mógłbyś podać symboliczne obliczenia? Jeśli istnieją operacje wektorowe, możesz naprawdę chcieć zbadać za pomocą blas lub lapack, które w niektórych przypadkach mogą wykonywać operacje równolegle.
Jest do pomyślenia (ryzykując brakiem tematu?), Że będziesz mógł używać Pythona z numpy i / lub scipy. O ile to możliwe, Twoje obliczenia mogą być bardziej czytelne.
źródło
Ponieważ wyraźnie zapytałeś o optymalizacje wysokiego poziomu, warto wypróbować różne kompilatory C ++. W dzisiejszych czasach kompilatory są bardzo złożonymi bestiami optymalizacyjnymi, a dostawcy procesorów mogą wdrażać bardzo potężne i szczegółowe optymalizacje. Pamiętaj jednak, że niektóre z nich nie są bezpłatne (ale może istnieć bezpłatny program akademicki).
Widziałem, że fragmenty kodu różnią się szybkością wykonywania o współczynnik 2, tylko przez zmianę kompilatora (oczywiście z pełną optymalizacją). Ale pamiętaj o sprawdzaniu tożsamości wyjścia. Agresywna optymalizacja może prowadzić do różnych wyników, czego zdecydowanie chcesz uniknąć.
Powodzenia!
źródło