Je me suis penché sur les désassemblages .NET et le code source de GCC, mais je n'arrive pas à trouver où que ce soit l'implémentation réelle de sin()
et d'autres fonctions mathématiques ... elles semblent toujours faire référence à autre chose.
Quelqu'un peut-il m'aider à les trouver? J'ai l'impression qu'il est peu probable que TOUT le matériel sur lequel C s'exécutera prend en charge les fonctions trig de matériel, il doit donc exister un algorithme logiciel quelque part, n'est-ce pas?
Je connais plusieurs façons de calculer les fonctions peut et j’ai écrit mes propres routines pour calculer les fonctions en utilisant des séries taylor pour le plaisir. Je suis curieux de voir à quel point les langages de production sont réels, car toutes mes implémentations sont toujours plus lentes de plusieurs ordres de grandeur, même si je pense que mes algorithmes sont assez intelligents (évidemment, ils ne le sont pas).
Dans GNU libm, la mise en oeuvre de sin
dépend du système. Par conséquent, vous pouvez trouver l’implémentation, pour chaque plate-forme, quelque part dans le sous-répertoire approprié de sysdeps .
Un répertoire comprend une implémentation en C, fournie par IBM. Depuis octobre 2011, il s'agit du code qui s'exécute lorsque vous appelez sin()
sur un système Linux x86-64 typique. Il est apparemment plus rapide que l'instruction fsin
Assembly. Code source: sysdeps/ieee754/dbl-64/s_sin.c , recherchez __sin (double x)
.
Ce code est très complexe. Aucun algorithme logiciel n’est aussi rapide et précis sur l’ensemble des valeurs x , de sorte que la bibliothèque implémente de nombreux algorithmes différents et que son premier travail est regarder x et décider quel algorithme utiliser. Dans certaines régions, il utilise ce qui ressemble à la série de Taylor habituelle. Plusieurs algorithmes calculent d'abord un résultat rapide, puis si ce n'est pas assez précis, ignorez-le et utilisez un algorithme plus lent.
Les anciennes versions 32 bits de GCC/glibc utilisaient l'instruction fsin
, ce qui est étonnamment imprécis pour certaines entrées. Il y a un billet de blog fascinant illustrant cela avec seulement 2 lignes de code .
l'implémentation de sin
en C pur par fdlibm est beaucoup plus simple que celle de glibc et est bien commentée. Code source: fdlibm/s_sin.c et fdlibm/k_sin.c
OK, les enfants, c'est le temps des pros ... C'est l'une de mes plus grosses plaintes avec des ingénieurs en logiciel peu expérimentés. Ils calculent des fonctions transcendantales à partir de rien (à l'aide de la série de Taylor) comme si personne n'avait fait ces calculs auparavant dans leur vie. Pas vrai. C'est un problème bien défini qui a été abordé des milliers de fois par des ingénieurs en logiciel et matériel très intelligents et qui propose une solution bien définie. Fondamentalement, la plupart des fonctions transcendantales utilisent les polynômes de Chebyshev pour les calculer. Les polynômes utilisés dépendent des circonstances. Premièrement, la Bible sur ce sujet est un livre intitulé "Computer Approximations" de Hart et Cheney. Dans ce livre, vous pouvez décider si vous avez un additionneur, un multiplicateur, un diviseur, etc., ainsi que les opérations les plus rapides. par exemple. Si vous avez un diviseur très rapide, le moyen le plus rapide de calculer le sinus peut être P1 (x)/P2 (x), où P1 et P2 sont des polynômes de Chebyshev. Sans le diviseur rapide, ce pourrait être juste P (x), où P a beaucoup plus de termes que P1 ou P2 ... donc ce serait plus lent. La première étape consiste donc à déterminer votre matériel et ce qu’il peut faire. Ensuite, vous choisissez la combinaison appropriée de polynômes de Chebyshev (généralement de la forme cos (ax) = aP (x) pour le cosinus, par exemple, où P est un polynôme de Chebyshev). Ensuite, vous décidez quelle précision décimale vous souhaitez. par exemple. si vous voulez une précision de 7 chiffres, recherchez-la dans le tableau approprié du livre que j'ai mentionné, et cela vous donnera (pour une précision de 7,33) un nombre N = 4 et un nombre polynomial 3502. N est l'ordre du polynôme. (donc p4.x ^ 4 + p3.x ^ 3 + p2.x ^ 2 + p1.x + p0), car N = 4. Ensuite, vous recherchez la valeur réelle des valeurs p4, p3, p2, p1, p0 à la fin du livre sous 3502 (elles seront en virgule flottante). Ensuite, vous implémentez votre algorithme dans le logiciel sous la forme: (((p4.x + p3) .x + p2) .x + p1) .x + p0 .... et voici comment vous calculeriez le cosinus à 7 décimales place sur ce matériel.
Notez que la plupart des implémentations matérielles des opérations transcendantales dans une FPU impliquent généralement un microcode et des opérations similaires (cela dépend du matériel). Les polynômes de Chebyshev sont utilisés pour la plupart des transcendantaux mais pas tous. par exemple. La racine carrée est plus rapide pour utiliser une double itération de la méthode de Newton Raphson en utilisant d'abord une table de correspondance. Encore une fois, ce livre "Computer Approximations" vous le dira.
Si vous envisagez d'implémenter ces fonctions, je recommanderais à quiconque de se procurer un exemplaire de ce livre. C'est vraiment la bible de ce type d'algorithmes. Notez qu'il existe plusieurs types de moyens alternatifs pour calculer ces valeurs, comme les cordiques, etc., mais ils ont tendance à être mieux adaptés à des algorithmes spécifiques nécessitant une faible précision. Pour garantir la précision à chaque fois, les polynômes de Chebyshev sont la voie à suivre. Comme je l'ai dit, problème bien défini. Résolu depuis 50 ans maintenant… et c'est comme ça que ça se passe.
Cela dit, il existe des techniques permettant d'utiliser les polynômes de Chebyshev pour obtenir un résultat simple précision avec un polynôme de degré faible (comme dans l'exemple ci-dessus pour le cosinus). Ensuite, il existe d'autres techniques d'interpolation entre les valeurs pour augmenter la précision sans avoir à passer à un polynôme beaucoup plus grand, tel que "la méthode des tables précises de Gal". Cette dernière technique est ce à quoi le poste faisant référence à la littérature ACM fait référence. Mais en fin de compte, les polynômes de Chebyshev sont utilisés pour obtenir 90% du chemin.
Prendre plaisir.
Des fonctions comme les sinus et les cosinus sont implémentées dans le microcode à l'intérieur des microprocesseurs. Les puces Intel, par exemple, ont des instructions d'assemblage à leur intention Un compilateur C générera un code appelant ces instructions d'assemblage. (En revanche, un compilateur Java ne le fera pas. Java évalue les fonctions trig dans le logiciel plutôt que le matériel, et s'exécute donc beaucoup plus lentement.)
Les puces n'utilisent pas les séries de Taylor pour calculer les fonctions trigonométriques, du moins pas tout à fait. Tout d’abord, ils utilisent CORDIC , mais ils peuvent également utiliser une courte série de Taylor pour améliorer le résultat de CORDIC ou pour des cas particuliers tels que le calcul du sinus avec une précision relative élevée pour les très petits angles. Pour plus d'explications, voir ceci réponse StackOverflow .
Pour sin
spécifiquement, l’extension de Taylor vous donnerait:
sin (x): = x - x ^ 3/3! + x ^ 5/5! - x ^ 7/7! + ... (1)
vous voudriez continuer à ajouter des termes jusqu'à ce que la différence entre eux soit inférieure à un niveau de tolérance accepté ou juste pour un nombre fini d'étapes (plus rapide, mais moins précis). Un exemple serait quelque chose comme:
float sin(float x)
{
float res=0, pow=x, fact=1;
for(int i=0; i<5; ++i)
{
res+=pow/fact;
pow*=-1*x*x;
fact*=(2*(i+1))*(2*(i+1)+1);
}
return res;
}
Remarque: (1) fonctionne à cause de l'aproximation sin (x) = x pour les petits angles. Pour des angles plus grands, vous devez calculer de plus en plus de termes pour obtenir des résultats acceptables. Vous pouvez utiliser un argument while et continuer avec une certaine précision:
double sin (double x){
int i = 1;
double cur = x;
double acc = 1;
double fact= 1;
double pow = x;
while (fabs(acc) > .00000001 && i < 100){
fact *= ((2*i)*(2*i+1));
pow *= -1 * x*x;
acc = pow / fact;
cur += acc;
i++;
}
return cur;
}
Oui, il existe aussi des algorithmes logiciels pour calculer sin
. Fondamentalement, le calcul de ce genre de choses avec un ordinateur numérique est généralement fait en utilisant méthodes numériques comme une approximation de série de Taylor représentant la fonction.
Les méthodes numériques peuvent approximer des fonctions avec une précision arbitraire. Etant donné que la précision que vous possédez dans un nombre flottant est finie, elles conviennent très bien à ces tâches.
C'est une question complexe. Les processeurs de type Intel de la famille x86 ont une implémentation matérielle de la fonction sin()
, mais font partie du FPU x87 et ne sont plus utilisés en mode 64 bits (où les registres SSE2 sont utilisés à la place). Dans ce mode, une implémentation logicielle est utilisée.
Il existe plusieurs implémentations de ce type. On est dans fdlibm et est utilisé en Java. Autant que je sache, l'implémentation glibc contient des parties de fdlibm et d'autres parties fournies par IBM.
Les implémentations logicielles de fonctions transcendantales telles que sin()
utilisent généralement des approximations par polynômes, souvent obtenues à partir des séries de Taylor.
Utilisez série de Taylor et essayez de trouver une relation entre les termes de la série pour ne pas calculer les choses encore et encore
Voici un exemple pour cosinus:
double cosinus(double x, double prec)
{
double t, s ;
int p;
p = 0;
s = 1.0;
t = 1.0;
while(fabs(t/s) > prec)
{
p++;
t = (-t * x * x) / ((2 * p - 1) * (2 * p));
s += t;
}
return s;
}
en utilisant cela, nous pouvons obtenir le nouveau terme de la somme en utilisant celui déjà utilisé (nous évitons la factorielle et x2p)
Les polynômes de Chebyshev, comme mentionné dans une autre réponse, sont les polynômes où la plus grande différence entre la fonction et le polynôme est aussi petite que possible. C'est un excellent début.
Dans certains cas, l'erreur maximale n'est pas ce qui vous intéresse, mais l'erreur relative maximale. Par exemple, pour la fonction sinus, l'erreur près de x = 0 devrait être beaucoup plus petite que pour des valeurs plus grandes; vous voulez une petite erreur relative . Donc, vous calculeriez le polynôme de Chebyshev pour sin x/x et multipliez ce polynôme par x.
Ensuite, vous devez trouver comment évaluer le polynôme. Vous voulez l’évaluer de telle sorte que les valeurs intermédiaires soient petites et donc les erreurs d’arrondi réduites. Sinon, les erreurs d'arrondi risquent de devenir beaucoup plus volumineuses que les erreurs du polynôme. Et avec des fonctions comme la fonction sinus, si vous êtes négligent, il est possible que le résultat que vous calculez pour sin x soit supérieur au résultat pour sin y même lorsque x <y. Le choix de l’ordre de calcul et le calcul des limites supérieures de l’erreur d’arrondi sont donc nécessaires.
Par exemple, sin x = x - x ^ 3/6 + x ^ 5/120 - x ^ 7/5040 ... Si vous calculez naïvement sin x = x * (1 - x ^ 2/6 + x ^ 4/120 - x ^ 6/5040 ...), la fonction entre parenthèses décroît et il se produira que si y est le nombre immédiatement supérieur à x, alors parfois le péché y sera plus petit que le péché x. Au lieu de cela, calculez péché x = x - x ^ 3 * (1/6 - x ^ 2/120 + x ^ 4/5040 ...) où cela ne peut pas se produire.
Lors du calcul des polynômes de Chebyshev, vous devez généralement arrondir les coefficients pour obtenir une double précision, par exemple. Mais si un polynôme de Chebyshev est optimal, le polynôme de Chebyshev avec des coefficients arrondis à la double précision n'est pas le polynôme optimal avec des coefficients de double précision!
Par exemple, pour sin (x), où vous avez besoin de coefficients pour x, x ^ 3, x ^ 5, x ^ 7, etc., procédez comme suit: Calculez la meilleure approximation de sin x avec un polynôme (ax + bx ^ 3 + cx ^ 5 + dx ^ 7) avec une précision supérieure à la double précision, puis arrondissez a à une double précision, ce qui donne A. La différence entre a et A serait assez grande. Calculons maintenant la meilleure approximation de (sin x - Ax) avec un polynôme (b x ^ 3 + cx ^ 5 + dx ^ 7). Vous obtenez des coefficients différents, car ils s'adaptent à la différence entre a et A. Arrondissez b à la double précision B. Ensuite, approximez-vous (sin x - Ax - Bx ^ 3) avec un polynôme cx ^ 5 + dx ^ 7, etc. Vous obtiendrez un polynôme presque aussi bon que le polynôme de Chebyshev original, mais bien meilleur que celui de Chebyshev, arrondi à la double précision.
Ensuite, vous devez prendre en compte les erreurs d'arrondi dans le choix du polynôme. Vous avez trouvé un polynôme avec une erreur minimale dans le polynôme en ignorant l'erreur d'arrondi, mais vous souhaitez optimiser l'erreur polynôme plus l'arrondi. Une fois que vous avez le polynôme de Chebyshev, vous pouvez calculer les bornes de l'erreur d'arrondi. Disons que f (x) est votre fonction, P (x) le polynôme et E (x) l’erreur d’arrondi. Vous ne voulez pas optimiser | f (x) - P (x) |, vous souhaitez optimiser | f (x) - P (x) +/- E (x) |. Vous obtiendrez un polynôme légèrement différent qui essaiera de limiter les erreurs polynomiales là où l'erreur d'arrondi est grande et atténue un peu les erreurs polynomiales lorsque l'erreur d'arrondi est petite.
Vous obtiendrez ainsi facilement des erreurs d'arrondi d'au plus 0,55 fois le dernier bit, où +, -, *,/auront des erreurs d'arrondi d'au plus 0,50 fois le dernier bit.
En ce qui concerne les fonctions trigonométriques telles que sin()
, cos()
, tan()
aucune mention, après 5 ans, d'un aspect important des fonctions trigonométriques de haute qualité: Réduction de la plage .
L'une des premières étapes de l'une ou l'autre de ces fonctions consiste à réduire l'angle, en radians, à l'intervalle 2 * π. Mais π est irrationnel et des réductions simples telles que x = remainder(x, 2*M_PI)
introduisent une erreur, car M_PI
, ou machine pi, est une approximation de π. Alors, comment faire x = remainder(x, 2*π)
?
Les premières bibliothèques utilisaient une programmation de précision étendue ou spécialement conçue pour obtenir des résultats de qualité, mais sur une plage limitée de double
. Lorsqu'une grande valeur était demandée, comme sin(pow(2,30))
, les résultats étaient sans signification ou 0.0
et peut-être avec un indicateur d'erreur réglé sur quelque chose comme TLOSS
perte totale de précision ou PLOSS
perte partielle de précision.
Une bonne réduction de la plage de valeurs importantes à un intervalle tel que -π à π est un problème complexe qui rivalise avec les défis de la fonction trig de base, comme sin()
, elle-même.
Un bon rapport est Réduction des arguments pour les arguments énormes: Bon jusqu'au dernier bit (1992). Il couvre bien le problème: discute du besoin et de la manière dont les choses se passaient sur différentes plates-formes (SPARC, PC, HP, plus de 30 autres) et fournit un algorithme de solution qui donne des résultats de qualité pour tous double
de -DBL_MAX
à DBL_MAX
.
Si les arguments d'origine sont exprimés en degrés et peuvent néanmoins avoir une valeur importante, utilisez d'abord fmod()
pour une précision améliorée. Un bon fmod()
introduira pas d'erreur et fournira ainsi une excellente réduction de la portée.
// sin(degrees2radians(x))
sin(degrees2radians(fmod(x, 360.0))); // -360.0 < fmod(x,360) < +360.0
Différentes identités de trigonomètres et remquo()
offrent encore plus d'amélioration. Exemple: sind ()
L'implémentation réelle des fonctions de la bibliothèque dépend du compilateur et/ou du fournisseur de la bibliothèque. Qu'il s'agisse de matériel ou de logiciel, d'une extension Taylor ou non, etc., cela varie.
Je réalise que ce n'est absolument pas une aide.
Comme beaucoup de personnes l'ont souligné, cela dépend de la mise en œuvre. Mais si j'ai bien compris votre question, vous vous intéressiez à une implémentation réelle de logiciels de fonctions mathématiques, mais vous n'avez pas réussi à en trouver une. Si tel est le cas, vous êtes ici:
dosincos.c
situé dans le répertoire racine de la glibc non compressé \sysdeps\ieee754\dbl-64Vous pouvez également consulter les fichiers portant l’extension .tbl
. Leur contenu n’est autre que d’énormes tables de valeurs précalculées de différentes fonctions sous une forme binaire. C’est la raison pour laquelle l’implémentation est si rapide: au lieu de calculer tous les coefficients de la série qu’ils utilisent, ils effectuent simplement une recherche rapide qui est beaucoup plus rapide . BTW, ils utilisent la série Tailor pour calculer le sinus et le cosinus.
J'espère que ça aide.
Je vais essayer de répondre au cas de sin()
dans un programme C, compilé avec le compilateur C de GCC sur un processeur x86 actuel (par exemple, un processeur Intel Core 2 Duo).
Dans le langage C, la bibliothèque Standard C comprend des fonctions mathématiques communes, non incluses dans le langage lui-même (par exemple, pow
, sin
et cos
pour les puissances, les sinus et les cosinus, respectivement). Les en-têtes dont sont inclus math.h .
Désormais, sur un système GNU/Linux, ces fonctions de bibliothèque sont fournies par glibc (GNU libc ou GNU C Library). Mais le compilateur GCC souhaite que vous vous connectiez à bibliothèque mathématique (libm.so
) à l'aide de l'indicateur -lm
pour permettre l'utilisation de ces fonctions mathématiques. Je ne sais pas pourquoi cela ne fait pas partie de la bibliothèque standard C. Il s’agit d’une version logicielle des fonctions à virgule flottante, ou "soft-float".
De plus: La raison de séparer les fonctions mathématiques est historique et visait simplement à réduire la taille des programmes exécutables dans à mon avis très anciens systèmes Unix, peut-être avant que les bibliothèques partagées ne soient disponibles.
Le compilateur peut maintenant optimiser la fonction standard de la bibliothèque C sin()
(fournie par libm.so
) à remplacer par un appel à une instruction native de la fonction sin () intégrée de votre CPU/FPU, qui existe déjà. une instruction FPU (FSIN
pour x86/x87) sur des processeurs plus récents, tels que la série Core 2 (ceci est exact presque aussi loin que l'i486DX). Cela dépend des indicateurs d'optimisation transmis au compilateur gcc. Si on demandait au compilateur d'écrire du code qui s'exécuterait sur un processeur i386 ou plus récent, il ne procéderait pas à une telle optimisation. L’indicateur -mcpu=486
informera le compilateur qu’une telle optimisation peut être effectuée en toute sécurité.
Maintenant, si le programme exécutait la version logicielle de la fonction sin (), il le ferait sur la base de CORDIC (calculateur de rotation coordonné) ou algorithme BKM , ou more probablement un calcul de table ou de série de puissances qui est couramment utilisé maintenant pour calculer de telles fonctions transcendantales. [Src: http://en.wikipedia.org/wiki/Cordic#Application]
Toute version récente (depuis environ 2,9 x environ) de gcc offre également une version intégrée de sin, __builtin_sin()
, qu'elle utilisera pour remplacer l'appel standard de la version de la bibliothèque C, à titre d'optimisation.
Je suis sûr que cela est aussi clair que de la boue, mais j'espère vous donner plus d'informations que ce à quoi vous vous attendiez et beaucoup de points de départ pour en apprendre davantage sur vous-même.
Ils sont généralement implémentés dans un logiciel et n'utilisent pas les appels matériels correspondants (c'est-à-dire, un assemblage) dans la plupart des cas. Cependant, comme l'a souligné Jason, ces tâches sont spécifiques à la mise en œuvre.
Notez que ces routines logicielles ne font pas partie des sources du compilateur, mais se trouvent plutôt dans la bibliothèque correspondante, telle que le clib, ou la glibc pour le compilateur GNU. Voir http://www.gnu.org/software/libc/manual/html_mono/libc.html#Trig-Functions
Si vous souhaitez un meilleur contrôle, vous devez évaluer avec soin ce dont vous avez exactement besoin. Certaines des méthodes typiques sont l'interpolation des tables de consultation, l'appel de Assembly (qui est souvent lent) ou d'autres schémas d'approximation tels que Newton-Raphson pour les racines carrées.
Rien de tel que de toucher la source et de voir comment quelqu'un l'a fait dans une bibliothèque couramment utilisée; regardons une implémentation de la bibliothèque C en particulier. J'ai choisi uLibC.
Voici la fonction de péché:
http://git.uclibc.org/uClibc/tree/libm/s_sin.c
qui semble traiter quelques cas particuliers, puis effectue une réduction d’argument pour mapper l’entrée sur la plage [-pi/4, pi/4], (scinder l’argument en deux parties, une grosse partie et une queue) avant d'appeler
http://git.uclibc.org/uClibc/tree/libm/k_sin.c
qui fonctionne alors sur ces deux parties. S'il n'y a pas de queue, une réponse approximative est générée à l'aide d'un polynôme de degré 13. S'il y a une queue, vous obtenez un petit ajout correctif basé sur le principe que sin(x+y) = sin(x) + sin'(x')y
Si vous souhaitez une implémentation logicielle, et non matérielle, vous devez trouver une réponse définitive à cette question au chapitre 5 de Recettes numériques . Ma copie est dans une boîte, donc je ne peux pas donner de détails, mais la version courte (si je me souviens de ce droit) est que vous prenez tan(theta/2)
comme votre opération primitive et calculez les autres à partir de là. Le calcul est fait avec une approximation en série, mais c'est quelque chose qui converge beaucoup beaucoup plus rapidement qu'une série de Taylor.
Désolé, je ne peux plus republier sans mettre la main sur le livre.
Chaque fois qu'une telle fonction est évaluée, à un certain niveau, il est probable que:
S'il n'y a pas de support matériel, le compilateur utilisera probablement cette dernière méthode, n'émettant que du code assembleur (sans symboles de débogage), plutôt que d'utiliser une bibliothèque c, ce qui rend le traçage du code réel difficile dans votre débogueur.
Si vous voulez regarder la mise en œuvre GNU réelle de ces fonctions en C, consultez le dernier coffre de la glibc. Voir le bibliothèque GNU C .
Ne pas utiliser la série de Taylor. Les polynômes de Chebyshev sont à la fois plus rapides et plus précis, comme l'ont souligné quelques personnes ci-dessus. Voici une implémentation (à l’origine de la ROM ZX Spectrum): https://albertveli.wordpress.com/2015/01/10/zx-sine/
Le calcul sinus/cosinus/tangent est en réalité très facile à faire avec du code utilisant la série de Taylor. En écrire un vous-même prend environ 5 secondes.
L'ensemble du processus peut être résumé avec cette équation ici:
Voici quelques routines que j'ai écrites pour C:
double _pow(double a, double b) {
double c = 1;
for (int i=0; i<b; i++)
c *= a;
return c;
}
double _fact(double x) {
double ret = 1;
for (int i=1; i<=x; i++)
ret *= i;
return ret;
}
double _sin(double x) {
double y = x;
double s = -1;
for (int i=3; i<=100; i+=2) {
y+=s*(_pow(x,i)/_fact(i));
s *= -1;
}
return y;
}
double _cos(double x) {
double y = 1;
double s = -1;
for (int i=2; i<=100; i+=2) {
y+=s*(_pow(x,i)/_fact(i));
s *= -1;
}
return y;
}
double _tan(double x) {
return (_sin(x)/_cos(x));
}