Réponse courte:
Une spécialisation de pow(x, n)
à où n
est un nombre naturel est souvent utile pour les performances temporelles . Mais le générique de la bibliothèque standard pow()
fonctionne toujours assez bien ( étonnamment! ) À cette fin et il est absolument essentiel d'en inclure le moins possible dans la bibliothèque C standard afin qu'elle puisse être rendue aussi portable et aussi facile à implémenter que possible. D'un autre côté, cela ne l'empêche pas du tout d'être dans la bibliothèque standard C ++ ou la STL, que je suis presque sûr que personne n'envisage d'utiliser dans une sorte de plate-forme embarquée.
Maintenant, pour la réponse longue.
pow(x, n)
peut être rendu beaucoup plus rapide dans de nombreux cas en se spécialisant n
sur un nombre naturel. J'ai dû utiliser ma propre implémentation de cette fonction pour presque tous les programmes que j'écris (mais j'écris beaucoup de programmes mathématiques en C). L'opération spécialisée peut être effectuée à O(log(n))
temps, mais lorsqu'elle n
est petite, une version linéaire plus simple peut être plus rapide. Voici les implémentations des deux:
// 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;
}
(Je suis parti x
et la valeur de retour est double parce que le résultat de pow(double x, unsigned n)
va rentrer dans un double aussi souvent que possible pow(double, double)
.)
(Oui, pown
c'est récursif, mais casser la pile est absolument impossible car la taille maximale de la pile sera à peu près égale log_2(n)
et n
est un entier. Si n
est un entier de 64 bits, cela vous donne une taille de pile maximale d'environ 64. Aucun matériel n'a une telle extrême limitations de la mémoire, à l'exception de certains PIC douteux avec des piles matérielles qui ne font que 3 à 8 appels de fonction profonds.)
En ce qui concerne les performances, vous serez surpris de ce dont une variété de jardin pow(double, double)
est capable. J'ai testé cent millions d'itérations sur mon IBM Thinkpad âgé de 5 ans avec x
un nombre d'itérations n
égal à 10 et égal à 10. Dans ce scénario, j'ai pown_l
gagné. la glibc a pow()
pris 12,0 secondes utilisateur, pown
7,4 secondes utilisateur et pown_l
seulement 6,5 secondes utilisateur. Ce n'est donc pas trop surprenant. Nous nous attendions plus ou moins à cela.
Ensuite, j'ai laissé x
être constant (je l'ai mis à 2,5), et j'ai bouclé n
de 0 à 19 cent millions de fois. Cette fois, de façon assez inattendue, la glibc a pow
gagné, et par un glissement de terrain! Cela n'a pris que 2,0 secondes utilisateur. Mon a pown
pris 9,6 secondes et a pown_l
pris 12,2 secondes. Que s'est-il passé ici? J'ai fait un autre test pour le savoir.
J'ai fait la même chose que ci-dessus seulement avec x
égal à un million. Cette fois, a pown
gagné à 9,6 secondes. pown_l
a pris 12,2s et la glibc pow a pris 16,3s. Maintenant, c'est clair! la glibc pow
fonctionne mieux que les trois quand elle x
est basse, mais pire quand elle x
est élevée. Lorsque x
est élevé, pown_l
fonctionne mieux lorsqu'il n
est faible et pown
fonctionne mieux lorsqu'il x
est élevé.
Voici donc trois algorithmes différents, chacun capable de mieux fonctionner que les autres dans les bonnes circonstances. Ainsi, en fin de compte, ce qui à utiliser dépend probablement plus sur la façon dont vous prévoyez d'utiliser pow
, mais en utilisant la bonne version est la peine, et ayant toutes les versions est agréable. En fait, vous pourriez même automatiser le choix de l'algorithme avec une fonction comme celle-ci:
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);
}
Tant que x_expected
et n_expected
sont des constantes décidées au moment de la compilation, avec éventuellement d'autres mises en garde, un compilateur d'optimisation digne de ce nom supprimera automatiquement l' pown_auto
appel de fonction entier et le remplacera par le choix approprié des trois algorithmes. (Maintenant, si vous voulez vraiment essayer d' utiliser ceci, vous devrez probablement jouer un peu avec, car je n'ai pas vraiment essayé de compiler ce que j'avais écrit ci-dessus.;))
D'un autre côté, la glibc pow
fonctionne et la glibc est déjà assez grande. Le standard C est censé être portable, y compris vers divers périphériques embarqués (en fait, les développeurs embarqués du monde entier conviennent généralement que la glibc est déjà trop volumineuse pour eux), et il ne peut pas être portable si pour chaque fonction mathématique simple, il doit inclure tous les algorithme alternatif qui pourrait être utile. C'est pourquoi ce n'est pas dans la norme C.
note de bas de page: Lors des tests de performances temporelles, j'ai donné à mes fonctions des indicateurs d'optimisation relativement généreux ( -s -O2
) qui sont susceptibles d'être comparables, sinon pires, à ce qui a probablement été utilisé pour compiler la glibc sur mon système (archlinux), donc les résultats sont probablement juste. Pour un test plus rigoureux, je devrais compiler moi-même la glibc et je n'ai vraiment pas envie de faire ça. J'utilisais Gentoo, donc je me souviens du temps que cela prend, même lorsque la tâche est automatisée . Les résultats sont assez concluants (ou plutôt peu concluants) pour moi. Vous êtes bien sûr les bienvenus pour le faire vous-même.
Tour de bonus: une spécialisation de pow(x, n)
à tous les entiers est instrumentale si une sortie entière exacte est requise, ce qui se produit. Envisagez d'allouer de la mémoire pour un tableau à N dimensions avec p ^ N éléments. Obtenir p ^ N même par un entraînera un segfault éventuellement aléatoire.