Risposta breve:
Una specializzazione di pow(x, n)
dove n
è un numero naturale è spesso utile per le prestazioni temporali . Ma il generico della libreria standard pow()
funziona ancora abbastanza ( sorprendentemente! ) Bene per questo scopo ed è assolutamente fondamentale includerne il meno possibile nella libreria C standard in modo che possa essere reso il più portatile e il più facile da implementare possibile. D'altra parte, ciò non impedisce affatto che sia nella libreria standard C ++ o STL, che sono abbastanza sicuro che nessuno ha intenzione di utilizzare in una sorta di piattaforma incorporata.
Ora, per la risposta lunga.
pow(x, n)
può essere reso molto più veloce in molti casi specializzandosi n
su un numero naturale. Ho dovuto utilizzare la mia implementazione di questa funzione per quasi tutti i programmi che scrivo (ma scrivo molti programmi matematici in C). L'operazione specializzata può essere eseguita in O(log(n))
tempo, ma quando n
è piccola, una versione lineare più semplice può essere più veloce. Ecco le implementazioni di entrambi:
// 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;
}
(Ho lasciato x
e il valore restituito raddoppia perché il risultato di pow(double x, unsigned n)
si adatterà a un doppio più o meno tutte le volte che pow(double, double)
vorrà.)
(Sì, pown
è ricorsivo, ma rompere lo stack è assolutamente impossibile poiché la dimensione massima dello stack sarà approssimativamente uguale log_2(n)
ed n
è un numero intero. Se n
è un numero intero a 64 bit, si ottiene una dimensione massima dello stack di circa 64. Nessun hardware ha una dimensione così estrema limitazioni di memoria, ad eccezione di alcuni PIC ingannevoli con stack hardware che vanno solo da 3 a 8 chiamate di funzione in profondità.)
Per quanto riguarda le prestazioni, rimarrai sorpreso da ciò di cui pow(double, double)
è capace una varietà da giardino . Ho testato un centinaio di milioni di iterazioni sul mio Thinkpad IBM di 5 anni con x
uguale al numero di iterazioni e n
uguale a 10. In questo scenario, ha pown_l
vinto. glibc ha pow()
impiegato 12,0 secondi utente, pown
7,4 secondi utente e pown_l
solo 6,5 secondi utente. Quindi non è troppo sorprendente. Più o meno ce lo aspettavamo.
Quindi, ho lasciato x
essere costante (l'ho impostato a 2,5) e ho ripetuto n
da 0 a 19 cento milioni di volte. Questa volta, del tutto inaspettatamente, glibc ha pow
vinto, e con una frana! Ci sono voluti solo 2,0 secondi utente. Il mio ha pown
impiegato 9,6 secondi e pown_l
12,2 secondi. Cos'è successo qua? Ho fatto un altro test per scoprirlo.
Ho fatto la stessa cosa di sopra solo con x
pari a un milione. Questa volta, ha pown
vinto a 9.6 secondi. pown_l
ha preso 12.2s e glibc pow ha preso 16.3s. Ora è chiaro! glibc pow
funziona meglio dei tre quando x
è basso, ma peggio quando x
è alto. Quando x
è alto, pown_l
funziona al meglio quando n
è basso e pown
funziona meglio quando x
è alto.
Quindi ecco tre diversi algoritmi, ciascuno in grado di funzionare meglio degli altri nelle giuste circostanze. Quindi, in definitiva, quale utilizzare molto probabilmente dipende da come si sta pensando di usare pow
, ma usando la versione corretta è valsa la pena, e aventi tutte le versioni è bello. In effetti, potresti persino automatizzare la scelta dell'algoritmo con una funzione come questa:
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);
}
Fintanto che x_expected
e n_expected
sono costanti decise in fase di compilazione, insieme a eventuali altri avvertimenti, un compilatore di ottimizzazione degno di questo nome rimuoverà automaticamente l'intera pown_auto
chiamata di funzione e la sostituirà con la scelta appropriata dei tre algoritmi. (Ora, se hai davvero intenzione di tentare di usarlo , probabilmente dovrai giocarci un po ', perché non ho esattamente provato a compilare ciò che avevo scritto sopra.;))
D'altra parte, glibc pow
funziona e glibc è già abbastanza grande. Lo standard C dovrebbe essere portabile, anche su vari dispositivi embedded (infatti gli sviluppatori embedded ovunque generalmente concordano sul fatto che glibc è già troppo grande per loro), e non può essere portabile se per ogni semplice funzione matematica deve includere ogni algoritmo alternativo che potrebbe essere utile. Quindi, ecco perché non è nello standard C.
nota a piè di pagina: Durante il test delle prestazioni temporali, ho fornito alle mie funzioni flag di ottimizzazione relativamente generosi ( -s -O2
) che potrebbero essere paragonabili, se non peggiori, a ciò che è stato probabilmente usato per compilare glibc sul mio sistema (archlinux), quindi i risultati sono probabilmente giusto. Per un test più rigoroso, dovrei compilare glibc da solo e davvero non ho voglia di farlo. Usavo Gentoo, quindi ricordo quanto tempo ci vuole, anche quando l'attività è automatizzata . I risultati sono abbastanza conclusivi (o piuttosto inconcludenti) per me. Ovviamente sei il benvenuto a farlo da solo.
Bonus round: una specializzazione di pow(x, n)
tutti i numeri interi è fondamentale se è richiesto un output intero esatto, cosa che accade. Considera l'allocazione della memoria per un array N-dimensionale con p ^ N elementi. Ottenere p ^ N fuori anche solo da uno risulterà in un possibile segfault che si verifica in modo casuale.