Kurze Antwort:
Eine Spezialisierung pow(x, n)
, wo n
eine natürliche Zahl ist oft nützlich für Pünktlichkeit . Aber das Generikum der Standardbibliothek pow()
funktioniert für diesen Zweck immer noch ziemlich ( überraschend! ) Gut und es ist absolut wichtig, so wenig wie möglich in die Standard-C-Bibliothek aufzunehmen, damit sie so portabel und so einfach wie möglich implementiert werden kann. Auf der anderen Seite hindert das sie überhaupt nicht daran, in der C ++ - Standardbibliothek oder der STL zu sein, und ich bin mir ziemlich sicher, dass niemand vorhat, sie in einer Art eingebetteter Plattform zu verwenden.
Nun zur langen Antwort.
pow(x, n)
kann in vielen Fällen viel schneller gemacht werden, indem man sich n
auf eine natürliche Zahl spezialisiert. Ich musste für fast jedes Programm, das ich schreibe, meine eigene Implementierung dieser Funktion verwenden (aber ich schreibe viele mathematische Programme in C). Die spezialisierte Operation kann rechtzeitig durchgeführt O(log(n))
werden, aber wenn sie n
klein ist, kann eine einfachere lineare Version schneller sein. Hier sind Implementierungen von beiden:
// 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;
}
(Ich bin gegangen x
und der Rückgabewert verdoppelt sich, weil das Ergebnis von pow(double x, unsigned n)
ungefähr so oft wie möglich in ein Doppel passt pow(double, double)
.)
(Ja, pown
ist rekursiv, aber das Brechen des Stapels ist absolut unmöglich, da die maximale Stapelgröße ungefähr gleich ist log_2(n)
und n
eine Ganzzahl ist. Wenn n
es sich um eine 64-Bit-Ganzzahl handelt, erhalten Sie eine maximale Stapelgröße von etwa 64. Keine Hardware ist so extrem Speicherbeschränkungen, mit Ausnahme einiger zwielichtiger PICs mit Hardware-Stacks, die nur 3 bis 8 Funktionsaufrufe tief gehen.)
Was die Leistung angeht, werden Sie überrascht sein, wozu eine Gartensorte pow(double, double)
in der Lage ist. Ich habe hundert Millionen Iterationen auf meinem 5 Jahre alten IBM Thinkpad getestet x
, die der Iterationsnummer und n
10 entsprechen. In diesem Szenario habe ich pown_l
gewonnen. glibc pow()
dauerte 12,0 Benutzersekunden, pown
7,4 Benutzersekunden und pown_l
nur 6,5 Benutzersekunden. Das ist also nicht allzu überraschend. Wir haben das mehr oder weniger erwartet.
Dann lasse x
ich konstant sein (ich setze es auf 2,5) und habe n
hundert Millionen Mal von 0 auf 19 geschleift . Diesmal pow
gewann glibc ganz unerwartet und durch einen Erdrutsch! Es dauerte nur 2,0 Sekunden. Ich pown
brauchte 9,6 Sekunden und pown_l
12,2 Sekunden. Was ist hier passiert? Ich habe einen weiteren Test gemacht, um das herauszufinden.
Ich habe das Gleiche wie oben gemacht, nur mit x
einer Million. Diesmal pown
gewann bei 9,6s. pown_l
dauerte 12,2 s und glibc pow dauerte 16,3 s. Jetzt ist es klar! glibc pow
schneidet besser ab als die drei, wenn x
es niedrig ist, aber am schlechtesten, wenn x
es hoch ist. Wenn x
hoch ist, ist pown_l
die beste Leistung, wenn n
es niedrig ist, und pown
die beste Leistung, wenn x
es hoch ist.
Hier sind drei verschiedene Algorithmen, von denen jeder unter den richtigen Umständen eine bessere Leistung als die anderen erzielen kann. Also, letztlich, die am ehesten zu verwenden , hängt davon ab , wie Sie planen , über die Verwendung pow
, aber die richtige Version verwenden ist es wert, und ist schön , alle Versionen haben. Mit einer Funktion wie der folgenden können Sie sogar die Auswahl des Algorithmus automatisieren:
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);
}
Solange x_expected
und n_expected
zum Zeitpunkt der Kompilierung Konstanten festgelegt werden, entfernt ein Optimierungs-Compiler, der es wert ist, automatisch den gesamten pown_auto
Funktionsaufruf und ersetzt ihn durch die entsprechende Auswahl der drei Algorithmen. (Wenn Sie nun tatsächlich versuchen wollen, dies zu verwenden , müssen Sie wahrscheinlich ein wenig damit spielen, da ich nicht genau versucht habe , das zu kompilieren, was ich oben geschrieben habe .;))
Auf der anderen Seite pow
funktioniert glibc und glibc ist bereits groß genug. Der C-Standard soll portabel sein, auch für verschiedene eingebettete Geräte (tatsächlich sind sich eingebettete Entwickler überall im Allgemeinen einig, dass glibc bereits zu groß für sie ist), und er kann nicht portabel sein, wenn für jede einfache mathematische Funktion jede enthalten sein muss alternativer Algorithmus, könnte von nutzen sein. Deshalb ist es nicht im C-Standard.
Fußnote: Beim Testen der -s -O2
Zeitleistung habe ich meinen Funktionen relativ großzügige Optimierungsflags ( ) gegeben, die wahrscheinlich mit dem vergleichbar sind, wenn nicht sogar schlechter als das, was wahrscheinlich zum Kompilieren von glibc auf meinem System (archlinux) verwendet wurde. Die Ergebnisse sind also wahrscheinlich Messe. Für einen strengeren Test müsste ich glibc selbst kompilieren und ich habe wirklich keine Lust dazu. Früher habe ich Gentoo verwendet, daher erinnere ich mich, wie lange es dauert, auch wenn die Aufgabe automatisiert ist . Die Ergebnisse sind für mich schlüssig (oder eher nicht schlüssig) genug. Sie können dies natürlich gerne selbst tun.
Bonusrunde: Eine Spezialisierung pow(x, n)
auf alle Ganzzahlen ist von entscheidender Bedeutung, wenn eine genaue Ganzzahlausgabe erforderlich ist. Ziehen Sie in Betracht, Speicher für ein N-dimensionales Array mit p ^ N Elementen zuzuweisen. Wenn p ^ N auch nur um eins ausgeschaltet wird, tritt möglicherweise ein zufällig auftretender Segfault auf.