web-dev-qa-db-fra.com

Pourquoi pas int pow (int base, int exposant) dans les bibliothèques standard C ++?

Je sens que je dois juste être incapable de le trouver. Y a-t-il une raison pour que la fonction p ++ de c ++ n'implémente la fonction "power" que pour les fonctions float et double?

Je sais que la mise en œuvre est triviale, j'ai juste le sentiment de faire un travail qui devrait être dans une bibliothèque standard. Une fonction d’alimentation robuste (c’est-à-dire qui gère le débordement de manière cohérente et explicite) n’est pas amusante à écrire.

104
Dan O

En fait, c'est le cas.

Depuis C++ 11, il existe une implémentation basée sur un modèle de pow(int, int) --- et des cas encore plus généraux, voir (7) dans http://en.cppreference.com/w/cpp/ numérique/math/pow

1
Dima Pasechnik

A partir de C++11, des cas spéciaux ont été ajoutés à la suite de fonctions d'alimentation (et à d'autres). C++11 [c.math] /11 déclare, après avoir répertorié toutes les surcharges float/double/long double (mon soulignement, et paraphrasé):

De plus, il doit y avoir des surcharges supplémentaires suffisantes pour garantir que, si un argument correspondant à un paramètre double a le type double ou un type entier, tous les arguments correspondant aux paramètres double sont effectivement convertis en double.

Donc, fondamentalement, les paramètres entiers seront mis à niveau en doubles pour effectuer l'opération.


Avant C++11 (à l'époque où votre question était posée), il n'y avait pas de surcharge d'entiers.

Étant donné que je n'étais ni étroitement associé aux créateurs de C ni C++ à l'époque de leur création (même si je suis plutôt plutôt ancien), les comités ANSI/ISO qui ont créé les normes, c’est nécessairement un avis de ma part. J'aimerais penser que c'est informé , mais comme ma femme vous le dira (fréquemment et sans beaucoup d'encouragement), je me suis déjà trompé :-)

La supposition, pour ce que ça vaut, suit.

Je soupçonne que la raison pour laquelle l'original pré-ANSI C n'avait pas cette fonctionnalité est parce qu'elle était totalement inutile. Premièrement, il existait déjà un moyen parfaitement efficace d’exercer des puissances entières (avec des doubles et ensuite, il est simplement reconverti en un entier, en vérifiant le dépassement et le dépassement d’entier avant la conversion).

Deuxièmement, vous devez également vous rappeler que l’intention initiale de C était un langage de programmation systèmes , et on peut se demander si une virgule flottante est souhaitable dans cet arène. tout.

Dans la mesure où l'un de ses premiers cas d'utilisation consistait à coder UNIX, la virgule flottante aurait été pratiquement inutile. BCPL, sur lequel C était basé, n’a également aucune utilité pour les pouvoirs (il n’a pas du tout de virgule flottante, de mémoire).

En passant, un opérateur de réseau intégré aurait probablement été un opérateur binaire plutôt qu'un appel à une bibliothèque. Vous n'ajoutez pas deux entiers avec x = add (y, z) mais avec x = y + z - faisant partie du langage proprement dit plutôt que de la bibliothèque.

Troisièmement, étant donné que la mise en œuvre du pouvoir intégral est relativement triviale, il est presque certain que les développeurs du langage utiliseraient mieux leur temps en fournissant des informations plus utiles (voir ci-dessous les commentaires sur le coût d'opportunité).

Cela vaut également pour l'original C++. Dans la mesure où l'implémentation d'origine n'était en réalité qu'un traducteur produisant le code C, elle a transféré de nombreux attributs de C. Son intention initiale était C-with-classes, pas C-with-classes-plus-un-peu-de-extra-mathématique.

Pour savoir pourquoi cela n’a jamais été ajouté aux normes avant C++11, vous devez vous rappeler que les organismes de normalisation ont des directives spécifiques à suivre. Par exemple, ANSI C a été spécifiquement chargé de codifier la pratique existante, et non de créer un nouveau langage. Sinon, ils auraient pu devenir fous et nous donner Ada :-)

Des itérations ultérieures de cette norme comportent également des directives spécifiques et figurent dans les documents de justification (explications sur les raisons pour lesquelles le comité a pris certaines décisions, et non sur les justifications du libellé lui-même).

Par exemple, le document de justification C99 reprend spécifiquement deux des C89 principes directeurs qui limitent ce qui peut être ajouté:

  • Gardez la langue petite et simple.
  • Fournissez une seule façon de faire une opération.

Des directives (pas nécessairement celles spécifiques ) sont établies pour les groupes de travail individuels et limitent donc les comités C++ (et tous les autres groupes ISO).

En outre, les organismes de normalisation réalisent qu'il existe un coût d'opportunité (terme économique signifiant ce que vous devez renoncer à une décision prise) pour chaque décision prise. Par exemple, le coût d’opportunité de l’achat de cette machine à sous de 10 000 USD est constitué de relations cordiales (ou probablement de toutes les relations ) avec votre moitié pendant environ six mois.

Eric Gunnerson explique bien cela avec son - explication de 100 points pour expliquer pourquoi certaines choses ne sont pas toujours ajoutées aux produits Microsoft. En gros, une fonctionnalité commence à 100 points dans le trou, elle doit donc ajouter un peu de valeur être même considéré.

En d’autres termes, préféreriez-vous avoir un opérateur de puissance intégré (qui, honnêtement, tout codeur peu décent pourrait assembler en dix minutes) ou un multi-threading ajouté à la norme? Pour ma part, je préférerais disposer de ce dernier et ne pas avoir à maudire les différentes implémentations sous UNIX et Windows.

J'aimerais aussi voir des milliers et des milliers de collections de la bibliothèque standard (hachages, btrees, arbres rouge-noir, dictionnaire, cartes arbitraires, etc.) également, mais comme le dit la justification:

Une norme est un traité entre un réalisateur et un programmeur.

Et le nombre de responsables de la mise en œuvre au sein des organismes de normalisation dépasse de loin le nombre de programmeurs (ou du moins les programmeurs qui ne comprennent pas le coût d'opportunité). Si tout cela était ajouté, la prochaine norme C++ serait C++215x et serait probablement entièrement implémentée par les développeurs du compilateur trois cents ans plus tard.

Quoi qu’il en soit, c’est ma pensée (plutôt volumineuse) sur le sujet. Si seulement des votes étaient attribués sur la quantité plutôt que sur la qualité, je ferais sauter tous les autres de l'eau. Merci pour l'écoute :-)

57
paxdiablo

Quoi qu'il en soit, pour tout type d'intégrale de largeur fixe, presque toutes les paires d'entrées possibles débordent du type. A quoi sert de normaliser une fonction qui ne donne pas un résultat utile pour la grande majorité de ses entrées possibles?

Vous devez avoir un grand type d’entier pour rendre la fonction utile, et la plupart des bibliothèques d’entier volumineuses fournissent cette fonction.


Edit: Dans un commentaire sur la question, static_rtti écrit "La plupart des entrées causent un débordement? La même chose est vraie pour exp et double pow, je ne vois personne se plaindre." Ceci est une erreur.

Laissons de côté exp, parce que c'est à côté de la question (bien que cela rendrait mon cas plus fort), et concentrons-nous sur double pow(double x, double y). Pour quelle partie des paires (x, y) cette fonction fait-elle quelque chose d'utile (c'est-à-dire pas simplement un débordement ou un débordement)?

En fait, je vais me concentrer uniquement sur une petite partie des paires d'entrées pour lesquelles pow a un sens, car cela suffira à prouver ce que je veux dire: si x est positif et | y | <= 1, alors pow ne déborde pas et ne déborde pas. Cela comprend près du quart de toutes les paires à virgule flottante (exactement la moitié des nombres à virgule flottante non-NaN sont positifs et un peu moins de la moitié des nombres à virgule flottante non-NaN ont une magnitude inférieure à 1). Évidemment, il y a beaucoup d'autres paires d'entrées pour lesquelles pow produit des résultats utiles, mais nous avons vérifié qu'il s'agit d'au moins une quart de toutes les entrées.

Maintenant, regardons une fonction de puissance entière à largeur fixe (c'est-à-dire non bignum). Pour quelle partie des entrées ne déborde-t-il pas simplement? Pour maximiser le nombre de paires d'entrées significatives, la base doit être signée et l'exposant non signé. Supposons que la base et l'exposant ont tous deux n bits de large. Nous pouvons facilement obtenir une limite sur la partie des entrées qui ont du sens:

  • Si l'exposant est 0 ou 1, alors toute base est significative.
  • Si l'exposant est supérieur ou égal à 2, aucune base supérieure à 2 ^ (n/2) ne produit de résultat significatif.

Ainsi, sur les 2 ^ (2n) paires d’entrées, moins de 2 ^ (n + 1) + 2 ^ (3n/2) produisent des résultats significatifs. Si nous examinons ce qui est probablement l'utilisation la plus courante, les entiers 32 bits, cela signifie qu'un élément de l'ordre de 1/1000e de un pour cent des paires d'entrées ne déborde pas simplement.

39
Stephen Canon

Parce qu'il n'y a aucun moyen de représenter toutes les puissances entières dans un int de toute façon:

>>> print 2**-4
0.0625
10

C'est en fait une question intéressante. Un argument que je n'ai pas trouvé dans la discussion est le simple manque de valeurs de retour évidentes pour les arguments. Comptons les causes d'échec de la fonction hypthétique int pow_int(int, int).

  1. Débordement
  2. Résultat non défini pow_int(0,0)
  3. Le résultat ne peut pas être représenté pow_int(2,-1)

La fonction a au moins 2 modes de défaillance. Les entiers ne peuvent pas représenter ces valeurs, le comportement de la fonction dans ces cas doit être défini par la norme - et les programmeurs doivent être informés de la manière dont la fonction gère exactement ces cas.

Globalement, quitter la fonction semble être la seule option judicieuse. Le programmeur peut utiliser la version à virgule flottante avec tous les rapports d'erreur disponibles.

9
phoku

Réponse courte:

Une spécialisation de pow(x, n) en où n est un nombre naturel est souvent utile pour performance temporelle. Mais la fonction générique pow() de la bibliothèque standard fonctionne toujours très bien (étonnamment!), et il est absolument essentiel d'inclure le moins possible dans la bibliothèque standard C pour qu'elle puisse l'être. aussi portable et facile à mettre en œuvre que possible. D'un autre côté, cela ne l'empêche pas du tout de figurer dans la bibliothèque standard C++ ou dans la STL, que je suis à peu près sûr que personne ne prévoit d'utiliser dans une sorte de plateforme intégrée.

Maintenant, pour la réponse longue.

pow(x, n) peut être rendu beaucoup plus rapide dans de nombreux cas en 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 dans O(log(n)) time, mais lorsque n est petit, 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;
    }

(J'ai laissé x et la valeur de retour sous forme de doublons, car le résultat de pow(double x, unsigned n) tiendra dans un double environ aussi souvent que pow(double, double).)

(Oui, pown 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, ce qui vous donne une taille de pile maximale d’environ 64. Non le matériel a de telles limites de mémoire, à l’exception de certains PIC douteux avec des piles de matériel ne fonctionnant que de 3 à 8 fonctions appels profonds.)

En ce qui concerne les performances, vous serez surpris de voir de quoi 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 égal au nombre d'itérations et n égal à 10. Dans ce scénario, pown_l A été remporté. La glibc pow() a pris 12,0 secondes utilisateur, pown 7,4 secondes utilisateur et pown_l 6,5 secondes seulement. Donc ce n'est pas trop surprenant. Nous nous attendions plus ou moins à cela.

Ensuite, je laisse x être constant (je le règle à 2,5) et j’ai bouclé n de 0 à 19 cent millions de fois. Cette fois, de façon tout à fait inattendue, glibc pow a gagné, et par un glissement de terrain! Cela n'a pris que 2,0 secondes utilisateur. Mon pown a pris 9,6 secondes et pown_l A 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, pown a gagné à 9.6s. pown_l A pris 12.2s et glibc pow a pris 16.3s. Maintenant, c'est clair! La glibc pow donne de meilleurs résultats que les trois lorsque x est faible, mais pire lorsque x est élevé. Lorsque x est élevé, pown_l Fonctionne mieux lorsque n est faible et pown fonctionne mieux lorsque x est élevé.

Voici donc trois algorithmes différents, chacun capable de fonctionner mieux que les autres dans les bonnes circonstances. Donc, en fin de compte, le choix le plus probable dépend de la manière dont vous envisagez d'utiliser pow, mais l'utilisation de la bonne version is en vaut la peine, et disposer de toutes les versions, c'est bien. En fait, vous pouvez 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 optimiseur digne de ce nom supprimera automatiquement l'appel de fonction pown_auto Et remplacez-le par le choix approprié des trois algorithmes. (Maintenant, si vous essayez réellement de tiliser ceci, vous devrez probablement jouer avec un peu, parce que je n'ai pas exactement essayé compiler ce que je 'écrit ci-dessus.;))

D'autre part, glibc powfonctionne et la glibc est déjà assez grande. La norme C est supposée être portable, y compris pour divers périphériques embarqués (en fait, les développeurs embarqués dans le monde s'accordent généralement pour dire que la glibc est déjà trop grosse pour eux), et elle ne peut pas être portable si pour chaque simple fonction mathématique, il faut inclure tous les algorithmes alternatifs qui pourraient être utiles. Donc, c'est pourquoi il n'est pas dans la norme C.

note de bas de page: lors des tests de performances temporelles, j'ai attribué à mes fonctions des indicateurs d'optimisation relativement généreux (-s -O2) susceptibles d'être comparables, voire pire, à ceux utilisés pour compiler la glibc sur mon système (archlinux) , donc les résultats sont probablement justes. Pour un test plus rigoureux, je devrais compiler moi-même la glibc et moi vraiment je n'ai pas envie de le faire. J'avais l'habitude d'utiliser Gentoo, donc je me souviens combien de temps 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 pouvez bien sûr le faire vous-même.

Bonus round: Une spécialisation de pow(x, n) sur tous les entiers est instrumental 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 éventuellement une erreur de segmentation aléatoire.

7
enigmaticPhysicist

Une des raisons pour lesquelles C++ ne doit pas avoir de surcharges supplémentaires est d'être compatible avec C.

C++ 98 a des fonctions comme double pow(double, int), mais celles-ci ont été supprimées dans C++ 11 avec l'argument que C99 ne les a pas incluses.

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2011/n3286.html#55

Obtenir un résultat légèrement plus précis signifie également obtenir un résultat légèrement différent .

6
Bo Persson

Le monde évolue constamment, de même que les langages de programmation. Le quatrième partie du TR décimal C ¹ ajoute encore plus de fonctions à <math.h>. Deux familles de ces fonctions peuvent être intéressantes pour cette question:

  • Les fonctions pown prennent un nombre à virgule flottante et un exposant intmax_t.
  • Les fonctions powr, qui prennent deux nombres à virgule flottante (x et y) et calculent x à la puissance y avec la formule exp(y*log(x)).

Il semble que les utilisateurs standard aient finalement jugé ces fonctionnalités suffisamment utiles pour être intégrées à la bibliothèque standard. Cependant, le rationnel est que ces fonctions sont recommandées par la norme ( ISO/IEC/IEEE 60559: 2011 pour les valeurs binaire et décimale nombres à virgule flottante. Je ne peux pas dire avec certitude quelle "norme" a été suivie à l'époque de C89, mais les évolutions futures de <math.h> Seront probablement fortement influencées par les évolutions futures de la Norme ISO/IEC/IEEE 60559 .

Notez que la quatrième partie du nombre décimal TR ne sera pas incluse dans C2x (la prochaine révision majeure en C), et sera probablement incluse ultérieurement en tant que fonctionnalité optionnelle. À ma connaissance, il n’ya eu aucune intention d’inclure cette partie du TR dans une future révision C++.


¹ Vous pouvez trouver de la documentation sur les travaux en cours ici .

3
Morwenn

Peut-être parce que l'ALU du processeur n'a pas implémenté une telle fonction pour les entiers, mais qu'il existe une telle instruction FPU (comme le souligne Stephen, c'est en fait une paire). Il était donc plus rapide de doubler, d'appeler pow avec des doublons, puis de tester le dépassement de capacité et de rejeter le renvoi, plutôt que de l'implémenter à l'aide d'arithmétique entière.

(d'une part, les logarithmes réduisent les pouvoirs de multiplication, mais les logarithmes d'entiers perdent beaucoup de précision pour la plupart des entrées)

Stephen a raison de dire que ce n'est plus vrai sur les processeurs modernes, mais le standard C lorsque les fonctions mathématiques ont été sélectionnées (le C++ vient d'utiliser les fonctions C) a maintenant quoi, 20 ans?

2
Ben Voigt