Krótka odpowiedź:
Specjalizacja pow(x, n)
gdzie n
jest liczbą naturalną jest często przydatna przy wykonywaniu zadań czasowych . Ale typowa biblioteka standardowa pow()
nadal działa całkiem (o dziwo! ) Dobrze w tym celu i absolutnie krytyczne jest włączenie jak najmniejszej ilości do standardowej biblioteki C, aby można ją było uczynić tak przenośną i tak łatwą do wdrożenia, jak to tylko możliwe. Z drugiej strony, to wcale nie przeszkadza mu być w standardowej bibliotece C ++ lub STL, którego jestem prawie pewien, że nikt nie planuje używać na jakiejś wbudowanej platformie.
A teraz długa odpowiedź.
pow(x, n)
w wielu przypadkach można znacznie przyspieszyć, specjalizując n
się w liczbach naturalnych. Musiałem używać własnej implementacji tej funkcji dla prawie każdego programu, który piszę (ale piszę wiele programów matematycznych w języku C). Specjalistyczną operację można wykonać w O(log(n))
czasie, ale gdy n
jest mała, prostsza wersja liniowa może być szybsza. Oto implementacje obu:
// Computes x^n, where n is a natural number.
double pown(double x, unsigned n)
{
double y = 1;
// n = 2*d + r. x^n = (x^2)^d * x^r.
unsigned d = n >> 1;
unsigned r = n & 1;
double x_2_d = d == 0? 1 : pown(x*x, d);
double x_r = r == 0? 1 : x;
return x_2_d*x_r;
}
// The linear implementation.
double pown_l(double x, unsigned n)
{
double y = 1;
for (unsigned i = 0; i < n; i++)
y *= x;
return y;
}
(Wyszedłem x
i wartość zwracana jako podwaja się, ponieważ wynik pow(double x, unsigned n)
będzie pasował do podwojenia mniej więcej tak często, jak pow(double, double)
będzie to możliwe).
(Tak, pown
jest rekurencyjny, ale zerwanie stosu jest absolutnie niemożliwe, ponieważ maksymalny rozmiar stosu będzie z grubsza równy log_2(n)
i n
jest liczbą całkowitą. Jeśli n
jest to 64-bitowa liczba całkowita, daje to maksymalny rozmiar stosu wynoszący około 64. Żaden sprzęt nie ma tak ekstremalnych możliwości ograniczenia pamięci, z wyjątkiem niektórych podejrzanych PIC ze stosami sprzętowymi, które sięgają tylko 3 do 8 wywołań funkcji głęboko.)
Jeśli chodzi o wydajność, będziesz zaskoczony, do czego pow(double, double)
zdolna jest odmiana ogrodowa . Przetestowałem sto milionów iteracji na moim 5-letnim IBM Thinkpad z x
równą liczbą iteracji i n
równą 10. W tym scenariuszu pown_l
wygrałem. glibc pow()
zajmował 12,0 sekund użytkownika, pown
7,4 sekundy użytkownika i pown_l
tylko 6,5 sekundy użytkownika. Więc to nie jest zbyt zaskakujące. Spodziewaliśmy się tego mniej więcej.
Następnie pozostawiam x
stałą wartość (ustawiam ją na 2,5) i wykonuję pętlę n
od 0 do 19 sto milionów razy. Tym razem, dość nieoczekiwanie, pow
wygrał glibc , i to po miażdżąco! Zajęło to tylko 2,0 sekundy użytkownika. Mój pown
zajął 9,6 sekundy i pown_l
12,2 sekundy. Co tu się stało? Zrobiłem kolejny test, aby się dowiedzieć.
Zrobiłem to samo, co powyżej, tylko z x
równym milionem. Tym razem pown
wygrał przy 9,6 s. pown_l
zajęło 12,2 s, a glibc pow 16,3 s. Teraz jest jasne! glibc pow
działa lepiej niż te trzy, gdy x
jest niski, ale gorzej, gdy x
jest wysoki. Kiedy x
jest wysoka, pown_l
działa najlepiej, gdy n
jest niska, i pown
działa najlepiej, gdy x
jest wysoka.
Oto trzy różne algorytmy, z których każdy może działać lepiej niż inne w odpowiednich okolicznościach. Ostatecznie więc, który z nich najprawdopodobniej zależy od tego, jak planujesz używać pow
, ale użycie właściwej wersji jest tego warte, a posiadanie wszystkich wersji jest przyjemne. W rzeczywistości możesz nawet zautomatyzować wybór algorytmu za pomocą takiej funkcji:
double pown_auto(double x, unsigned n, double x_expected, unsigned n_expected) {
if (x_expected < x_threshold)
return pow(x, n);
if (n_expected < n_threshold)
return pown_l(x, n);
return pown(x, n);
}
O ile wartości stałe x_expected
i n_expected
są ustalane w czasie kompilacji, wraz z prawdopodobnie kilkoma innymi zastrzeżeniami, warty swojej soli kompilator optymalizujący automatycznie usunie całe pown_auto
wywołanie funkcji i zastąpi je odpowiednim wyborem spośród trzech algorytmów. (Teraz, jeśli rzeczywiście zamierzasz tego użyć , prawdopodobnie będziesz musiał się trochę bawić, ponieważ nie próbowałem dokładnie skompilować tego, co napisałem powyżej.;))
Z drugiej strony, glibc pow
działa, a glibc jest już wystarczająco duży. Standard C ma być przenośny, w tym na różne urządzenia wbudowane (w rzeczywistości deweloperzy oprogramowania wbudowanego na całym świecie ogólnie zgadzają się, że glibc jest już dla nich za duży) i nie może być przenośny, jeśli dla każdej prostej funkcji matematycznej musi zawierać wszystkie alternatywny algorytm, który może być przydatny. Dlatego właśnie nie jest w standardzie C.
przypis: Podczas testowania wydajności w czasie dałem moim funkcjom stosunkowo duże flagi optymalizacji ( -s -O2
), które prawdopodobnie będą porównywalne, jeśli nie gorsze, z tym, co prawdopodobnie zostało użyte do kompilacji glibc w moim systemie (archlinux), więc wyniki są prawdopodobnie targi. Aby uzyskać bardziej rygorystyczny test, musiałbym sam skompilować glibc i naprawdę nie mam na to ochoty. Kiedyś używałem Gentoo, więc pamiętam, ile czasu to zajmuje, nawet jeśli zadanie jest zautomatyzowane . Wyniki są dla mnie wystarczająco rozstrzygające (lub raczej niejednoznaczne). Oczywiście możesz to zrobić samodzielnie.
Runda bonusowa: specjalizacja pow(x, n)
wszystkich liczb całkowitych ma znaczenie instrumentalne, jeśli wymagana jest dokładna liczba całkowita, co się zdarza. Rozważ przydzielenie pamięci dla tablicy N-wymiarowej z elementami p ^ N. Usunięcie p ^ N nawet o jeden spowoduje prawdopodobnie przypadkowe wystąpienie błędu.