Co jest bardziej wydajne? Używając pow do kwadratu, czy po prostu pomnóż to przez siebie?


119

Która z tych dwóch metod jest bardziej wydajna w C? I jak:

pow(x,3)

vs.

x*x*x // etc?

9
Czy jest xcałka czy zmiennoprzecinkowa?
Matthew Flaschen

6
Możesz spróbować napisać program, który wykonuje powyższe dwie operacje i ile czasu zajmie wykonanie z biblioteką profilowania. To da ci dobrą odpowiedź pod względem czasu wykonania.
J. Polfer

3
Kiedy mówisz wydajne, masz na myśli czas czy przestrzeń (tj. Użycie pamięci)?
J. Polfer

4
@sheepsimulator: +1 za zaoszczędzenie mi czasu potrzebnego na (ponownie) wskazanie, że napisanie szybkiego testu da ci ostateczną odpowiedź szybciej niż otrzymasz potencjalnie niejasną lub niepoprawną odpowiedź od SO.
TYLKO MOJA poprawna OPINIA

5
@kirill_igum Jeśli są to wartości zmiennoprzecinkowe, które nie są błędem, arytmetyka zmiennoprzecinkowa nie jest asocjacyjna.
effeffe

Odpowiedzi:


82

Testowałem różnicę wydajności między x*x*...vs pow(x,i)dla małych iprzy 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 = 1000000lotrzymam:

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*xbę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 stddyrektywę, jeśli drugi parametr to powto 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;
}

1
czy to oznacza, że ​​wstawienie „using namespace std” powoduje wybranie opcji C i będzie to szkodliwe dla środowiska wykonawczego?
Andreas

W obu pętlach czasowych obliczenia pow prawdopodobnie są wykonywane tylko raz. gcc -O2 nie powinno mieć problemu z wyciągnięciem niezmiennego wyrażenia z pętli. Więc po prostu testujesz, jak dobrze kompilator radzi sobie z przekształcaniem pętli add-constant w multiply lub po prostu optymalizowaniem pętli add-constant. Jest powód, dla którego twoje pętle mają taką samą prędkość z wykładnikiem = 1 w porównaniu z wykładnikiem = 5, nawet dla wersji zapisanej.
Peter Cordes

2
Wypróbowałem to na godbolt (z wykomentowanym czasem, ponieważ godbolt nie ma zainstalowanego Boost). Zaskakująco faktycznie wywołuje std::pow8 * 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ą.)
Peter Cordes

Głosowałem w dół, zanim zdałem sobie sprawę, że miałem włączoną matematykę -ffast podczas sesji godbolt, której używałem. Nawet bez tego testy testpow <1> i testpow <2> są zepsute, ponieważ są one w linii z powwywoł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 test5bę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.
Peter Cordes

@PeterCordes, gdzie byłeś 5 lat temu? :-) Spróbuję naprawić mój wzorzec, stosując się powdo ciągle zmieniającej się wartości (aby zapobiec wyciągnięciu powtarzającego się wyrażenia pow).
Emile Cormier

30

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źć).


43
To może być właściwe pytanie. Może nie myśli o swoim własnym projekcie praktycznym, a jedynie interesuje go, jak działa
język

137
Stackoverflow powinien mieć przycisk, który wstawia standardowe zastrzeżenie: „Wiem już, że przedwczesna optymalizacja jest zła, ale zadaję to pytanie optymalizacyjne do celów akademickich lub już zidentyfikowałem ten wiersz / blok kodu jako wąskie gardło”.
Emile Cormier

39
Nie sądzę, aby czytelność była tutaj problemem. Pisanie x * x versus pow (x, 2) wydaje się być całkiem jasne.
KillianDS

41
Nadmierne użycie pogrubienia i kursywy, nie jest łatwe dla oczu.
Stagi

24
Nie do końca zgadzam się z tą odpowiedzią. To ważne pytanie o wydajność. Najlepsza wydajność, jaką możesz osiągnąć, jest czasem ważnym wymaganiem, a często powodem, dla którego ktoś użył C ++ zamiast innego języka. Pomiar nie zawsze jest dobrym pomysłem. Mógłbym zmierzyć sortowanie bąbelkowe i szybkie sortowanie i szybciej znaleźć sortowanie bąbelkowe z moimi 10 pozycjami, ponieważ nie miałem tła, aby wiedzieć, że liczba elementów ma ogromne znaczenie i później stwierdziłem, że przy moich milionach przedmiotów był to bardzo zły wybór.
jcoder

17

x*xlub x*x*xbędzie szybszy niż pow, ponieważ powmusi zająć się przypadkiem ogólnym, podczas gdy x*xjest 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.


7
Myślałem o tym samym, dopóki nie zdecydowałem się go przetestować. Właśnie przetestowałem x*x*xkontra podwójne std::pow(double base, int exponent)w pętli czasowej i nie widzę statystycznie znaczącej różnicy w wydajności.
Emile Cormier

2
Upewnij się, że kompilator nie jest optymalizowany.
Ponkadoodle

1
@Emile: Sprawdź kod wygenerowany przez kompilator. Optymalizatory czasami robią trudne (i nieoczywiste) rzeczy. Sprawdź również wydajność na różnych poziomach optymalizacji: na przykład -O0, -O1, -O2 i -O3.
TYLKO MOJA poprawna OPINIA

2
Nie można zakładać, że uogólnione funkcje są wolniejsze. Czasami jest odwrotnie, ponieważ prostszy kod jest łatwiejszy do optymalizacji dla kompilatora.
kambunctious

5

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 ...


4

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

2

Jeśli wykładnik jest stały i mały, rozszerz go, minimalizując liczbę mnożeń. (Na przykład, x^4nie jest optymalnie x*x*x*x, ale y*ygdzie y=x*x. I x^5jest y*y*xgdzie 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 powma (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ć powi poprosić profilera, aby powiedzieć Ci, że to (nie jest zaskakujące) marnowanie czasu - eliminujesz ten krok, robiąc to inteligentnie.)


0

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

wprowadź opis obrazu tutaj

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.