Która z tych dwóch metod jest bardziej wydajna w C? I jak:
pow(x,3)
vs.
x*x*x // etc?
Która z tych dwóch metod jest bardziej wydajna w C? I jak:
pow(x,3)
vs.
x*x*x // etc?
Odpowiedzi:
Testowałem różnicę wydajności między x*x*...
vs pow(x,i)
dla małych i
przy użyciu tego kodu:
#include <cstdlib>
#include <cmath>
#include <boost/date_time/posix_time/posix_time.hpp>
inline boost::posix_time::ptime now()
{
return boost::posix_time::microsec_clock::local_time();
}
#define TEST(num, expression) \
double test##num(double b, long loops) \
{ \
double x = 0.0; \
\
boost::posix_time::ptime startTime = now(); \
for (long i=0; i<loops; ++i) \
{ \
x += expression; \
x += expression; \
x += expression; \
x += expression; \
x += expression; \
x += expression; \
x += expression; \
x += expression; \
x += expression; \
x += expression; \
} \
boost::posix_time::time_duration elapsed = now() - startTime; \
\
std::cout << elapsed << " "; \
\
return x; \
}
TEST(1, b)
TEST(2, b*b)
TEST(3, b*b*b)
TEST(4, b*b*b*b)
TEST(5, b*b*b*b*b)
template <int exponent>
double testpow(double base, long loops)
{
double x = 0.0;
boost::posix_time::ptime startTime = now();
for (long i=0; i<loops; ++i)
{
x += std::pow(base, exponent);
x += std::pow(base, exponent);
x += std::pow(base, exponent);
x += std::pow(base, exponent);
x += std::pow(base, exponent);
x += std::pow(base, exponent);
x += std::pow(base, exponent);
x += std::pow(base, exponent);
x += std::pow(base, exponent);
x += std::pow(base, exponent);
}
boost::posix_time::time_duration elapsed = now() - startTime;
std::cout << elapsed << " ";
return x;
}
int main()
{
using std::cout;
long loops = 100000000l;
double x = 0.0;
cout << "1 ";
x += testpow<1>(rand(), loops);
x += test1(rand(), loops);
cout << "\n2 ";
x += testpow<2>(rand(), loops);
x += test2(rand(), loops);
cout << "\n3 ";
x += testpow<3>(rand(), loops);
x += test3(rand(), loops);
cout << "\n4 ";
x += testpow<4>(rand(), loops);
x += test4(rand(), loops);
cout << "\n5 ";
x += testpow<5>(rand(), loops);
x += test5(rand(), loops);
cout << "\n" << x << "\n";
}
Wyniki są następujące:
1 00:00:01.126008 00:00:01.128338
2 00:00:01.125832 00:00:01.127227
3 00:00:01.125563 00:00:01.126590
4 00:00:01.126289 00:00:01.126086
5 00:00:01.126570 00:00:01.125930
2.45829e+54
Zwróć uwagę, że sumuję wynik każdego obliczenia pow, aby upewnić się, że kompilator go nie zoptymalizuje.
Jeśli użyję std::pow(double, double)
wersji i loops = 1000000l
otrzymam:
1 00:00:00.011339 00:00:00.011262
2 00:00:00.011259 00:00:00.011254
3 00:00:00.975658 00:00:00.011254
4 00:00:00.976427 00:00:00.011254
5 00:00:00.973029 00:00:00.011254
2.45829e+52
To jest na Intel Core Duo z systemem Ubuntu 9.10 64bit. Skompilowane przy użyciu gcc 4.4.1 z optymalizacją -o2.
Więc w C tak x*x*x
będzie szybciej niż pow(x, 3)
, ponieważ nie ma pow(double, int)
przeciążenia. W C ++ będzie to mniej więcej to samo. (Zakładając, że metodologia moich testów jest poprawna).
Oto odpowiedź na komentarz An Markm:
Nawet jeśli wydano using namespace std
dyrektywę, jeśli drugi parametr to pow
to a int
, wówczas zostanie wywołane std::pow(double, int)
przeciążenie from <cmath>
zamiast ::pow(double, double)
from <math.h>
.
Ten kod testowy potwierdza to zachowanie:
#include <iostream>
namespace foo
{
double bar(double x, int i)
{
std::cout << "foo::bar\n";
return x*i;
}
}
double bar(double x, double y)
{
std::cout << "::bar\n";
return x*y;
}
using namespace foo;
int main()
{
double a = bar(1.2, 3); // Prints "foo::bar"
std::cout << a << "\n";
return 0;
}
std::pow
8 * razy pętli (dla wykładnika> 2), chyba że używasz -fno-math-errno
. Wtedy może wyciągnąć wezwanie pow z pętli, tak jak myślałem. Wydaje mi się, że ponieważ errno jest globalne, bezpieczeństwo wątków wymaga, aby wywoływało pow, aby prawdopodobnie ustawić errno wiele razy ... exp = 1 i exp = 2 są szybkie, ponieważ wywołanie pow jest wyciągane z pętli tylko -O3
... ( z - ffast-math , robi też sumę-8 poza pętlą.)
pow
wywołaniem wyciągniętym z pętli, więc jest tam duży błąd. Wygląda też na to, że głównie testujesz opóźnienie dodawania FP, ponieważ wszystkie testy są wykonywane w tym samym czasie. Spodziewałbyś się, że test5
będzie wolniejszy niż test1
, ale tak nie jest. Użycie wielu akumulatorów podzieliłoby łańcuch zależności i ukryłoby opóźnienie.
pow
do ciągle zmieniającej się wartości (aby zapobiec wyciągnięciu powtarzającego się wyrażenia pow).
To niewłaściwe pytanie. Właściwe pytanie brzmiałoby: „Który z nich jest łatwiejszy do zrozumienia dla ludzkich czytelników mojego kodu?”
Jeśli prędkość ma znaczenie (później), nie pytaj, ale mierz. (A wcześniej, zmierz, czy optymalizacja faktycznie spowoduje jakąkolwiek zauważalną różnicę.) Do tego czasu pisz kod tak, aby był najłatwiejszy do odczytania.
Edytuj
Tylko, aby to wyjaśnić (chociaż już powinno być): Przełomowe przyspieszenia zwykle wynikają z takich rzeczy, jak użycie lepszych algorytmów , poprawa lokalizacji danych , zmniejszenie użycia pamięci dynamicznej , wyniki obliczeń wstępnych itp. Rzadko kiedy pochodzą z mikro-optymalizacja wywołań pojedynczych funkcji , a gdzie to robią, robią to w bardzo niewielu miejscach , co można by znaleźć tylko przez staranne (i czasochłonne) profilowanie , częściej niż nigdy można je przyspieszyć, wykonując bardzo nieintuicyjne rzeczy (np. wstawianienoop
wypowiedzi), a to, co jest optymalizacją dla jednej platformy, jest czasami pesymizacją dla innej (dlatego zamiast pytać trzeba mierzyć, bo nie znamy / nie mamy do końca swojego środowiska).
Jeszcze raz podkreślę: nawet w nielicznych aplikacjach, w których takie rzeczy mają znaczenie, nie mają one znaczenia w większości miejsc, w których są używane, i jest bardzo mało prawdopodobne, że znajdziesz miejsca, w których mają one znaczenie, patrząc na kod. Naprawdę musisz najpierw zidentyfikować gorące punkty , ponieważ w przeciwnym razie optymalizacja kodu jest tylko stratą czasu .
Nawet jeśli pojedyncza operacja (taka jak obliczenie kwadratu o jakiejś wartości) zajmuje 10% czasu wykonywania aplikacji (co jest dość rzadkie w edytorze IME), a nawet optymalizacja oszczędza 50% czasu potrzebnego na tę operację (którym IME jest nawet dużo, dużo rzadziej), nadal sprawiałeś, że aplikacja była trwała tylko 5% mniej czasu .
Twoi użytkownicy będą potrzebować stopera, aby to zauważyć. (Chyba w większości przypadków coś pod 20% przyspieszenie niezauważone dla większości użytkowników. I to jest cztery takie miejsca trzeba znaleźć).
x*x
lub x*x*x
będzie szybszy niż pow
, ponieważ pow
musi zająć się przypadkiem ogólnym, podczas gdy x*x
jest konkretny. Możesz również wyeliminować wywołanie funkcji i tym podobne.
Jeśli jednak znajdziesz się w takiej mikrooptymalizacji, musisz zdobyć profilera i zrobić poważne profilowanie. Ogromne prawdopodobieństwo jest takie, że nigdy nie zauważysz żadnej różnicy między nimi.
x*x*x
kontra podwójne std::pow(double base, int exponent)
w pętli czasowej i nie widzę statystycznie znaczącej różnicy w wydajności.
Zastanawiałem się również nad problemem z wydajnością i miałem nadzieję, że zostanie on zoptymalizowany przez kompilator na podstawie odpowiedzi z @EmileCormier. Martwiłem się jednak, że kod testowy, który pokazał, nadal pozwoli kompilatorowi na optymalizację wywołania std :: pow (), ponieważ za każdym razem używane były te same wartości, co pozwoliłoby kompilatorowi na przechowywanie wyników i użyj go ponownie w pętli - wyjaśniłoby to prawie identyczne czasy wykonywania dla wszystkich przypadków. Więc też się temu przyjrzałem.
Oto kod, którego użyłem (test_pow.cpp):
#include <iostream>
#include <cmath>
#include <chrono>
class Timer {
public:
explicit Timer () : from (std::chrono::high_resolution_clock::now()) { }
void start () {
from = std::chrono::high_resolution_clock::now();
}
double elapsed() const {
return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() - from).count() * 1.0e-6;
}
private:
std::chrono::high_resolution_clock::time_point from;
};
int main (int argc, char* argv[])
{
double total;
Timer timer;
total = 0.0;
timer.start();
for (double i = 0.0; i < 1.0; i += 1e-8)
total += std::pow (i,2);
std::cout << "std::pow(i,2): " << timer.elapsed() << "s (result = " << total << ")\n";
total = 0.0;
timer.start();
for (double i = 0.0; i < 1.0; i += 1e-8)
total += i*i;
std::cout << "i*i: " << timer.elapsed() << "s (result = " << total << ")\n";
std::cout << "\n";
total = 0.0;
timer.start();
for (double i = 0.0; i < 1.0; i += 1e-8)
total += std::pow (i,3);
std::cout << "std::pow(i,3): " << timer.elapsed() << "s (result = " << total << ")\n";
total = 0.0;
timer.start();
for (double i = 0.0; i < 1.0; i += 1e-8)
total += i*i*i;
std::cout << "i*i*i: " << timer.elapsed() << "s (result = " << total << ")\n";
return 0;
}
Zostało to skompilowane przy użyciu:
g++ -std=c++11 [-O2] test_pow.cpp -o test_pow
Zasadniczo różnica polega na tym, że argumentem std :: pow () jest licznik pętli. Tak jak się obawiałem, różnica w wydajności jest wyraźna. Bez flagi -O2 wyniki w moim systemie (Arch Linux 64-bit, g ++ 4.9.1, Intel i7-4930) były następujące:
std::pow(i,2): 0.001105s (result = 3.33333e+07)
i*i: 0.000352s (result = 3.33333e+07)
std::pow(i,3): 0.006034s (result = 2.5e+07)
i*i*i: 0.000328s (result = 2.5e+07)
W przypadku optymalizacji wyniki były równie uderzające:
std::pow(i,2): 0.000155s (result = 3.33333e+07)
i*i: 0.000106s (result = 3.33333e+07)
std::pow(i,3): 0.006066s (result = 2.5e+07)
i*i*i: 9.7e-05s (result = 2.5e+07)
Wygląda więc na to, że kompilator przynajmniej próbuje zoptymalizować przypadek std :: pow (x, 2), ale nie przypadek std :: pow (x, 3) (zajmuje to ~ 40 razy dłużej niż std :: pow (x, 2) przypadek). We wszystkich przypadkach ręczne rozszerzanie działało lepiej - ale szczególnie w przypadku Power 3 (60 razy szybciej). Warto o tym pamiętać, uruchamiając std :: pow () z mocami całkowitymi większymi niż 2 w ciasnej pętli ...
Najbardziej efektywnym sposobem jest rozważenie wykładniczego wzrostu mnożenia. Sprawdź ten kod dla p ^ q:
template <typename T>
T expt(T p, unsigned q){
T r =1;
while (q != 0) {
if (q % 2 == 1) { // if q is odd
r *= p;
q--;
}
p *= p;
q /= 2;
}
return r;
}
Jeśli wykładnik jest stały i mały, rozszerz go, minimalizując liczbę mnożeń. (Na przykład, x^4
nie jest optymalnie x*x*x*x
, ale y*y
gdzie y=x*x
. I x^5
jest y*y*x
gdzie y=x*x
. I tak dalej.) Dla stałych wykładników całkowitych, po prostu napisz już zoptymalizowaną formę; z małymi wykładnikami jest to standardowa optymalizacja, która powinna być wykonywana niezależnie od tego, czy kod był profilowany, czy nie. Zoptymalizowany formularz będzie szybszy w tak dużym procencie przypadków, że w zasadzie zawsze warto to zrobić.
(Jeśli używasz Visual C ++, std::pow(float,int)
przeprowadza optymalizację, o której wspominam, gdzie sekwencja operacji jest powiązana ze wzorem bitowym wykładnika. Nie gwarantuję jednak, że kompilator rozwinie pętlę za Ciebie, więc nadal warto to robić to ręcznie.)
[edytuj] BTW pow
ma (nie) zaskakującą tendencję do pojawiania się na wynikach profilera. Jeśli nie jest to absolutnie potrzebne (tj. Wykładnik jest duży lub nie jest stałą) i w ogóle martwisz się o wydajność, najlepiej napisać optymalny kod i poczekać, aż profiler powie ci, że to jest (zaskakująco ) marnowanie czasu przed dalszym myśleniem. (Alternatywą jest zadzwonić pow
i poprosić profilera, aby powiedzieć Ci, że to (nie jest zaskakujące) marnowanie czasu - eliminujesz ten krok, robiąc to inteligentnie.)
Byłem zajęty podobnym problemem i wyniki mnie zaskakują. Obliczałem x⁻³ / ² dla grawitacji Newtona w sytuacji n-ciał (przyspieszenie od innego ciała o masie M znajdującego się na wektorze odległości d): a = M G d*(d²)⁻³/²
(gdzie d² jest iloczynem kropkowym (skalarnym) samego d), i pomyślałem, że obliczanie M*G*pow(d2, -1.5)
będzie prostsze niżM*G/d2/sqrt(d2)
Sztuczka polega na tym, że dotyczy to małych systemów, ale gdy systemy rosną, M*G/d2/sqrt(d2)
stają się bardziej wydajne i nie rozumiem, dlaczego rozmiar systemu wpływa na ten wynik, ponieważ powtarzanie operacji na różnych danych nie. To tak, jakby były możliwe optymalizacje w miarę rozwoju systemu, ale które nie są możliwe w przypadkupow
x
całka czy zmiennoprzecinkowa?