445 votes

Comment est implémentée Math.Pow() dans le .NET Framework?

J'étais à la recherche d'une approche efficace pour calculer ab (disons a = 2 et b = 50). Pour commencer, j'ai décidé de jeter un coup d'œil à l'implémentation de la fonction Math.Pow(). Mais dans .NET Reflector, tout ce que j'ai trouvé était ceci :

[MethodImpl(MethodImplOptions.InternalCall), SecuritySafeCritical]
public static extern double Pow(double x, double y);

Quels sont les ressources où je peux voir ce qui se passe lorsque j'appelle la fonction Math.Pow() ?

17 votes

Tout comme information, si vous êtes confus au sujet de l'ensemble InternalCall avec un modificateur extern (comme ils semblent être en conflit), veuillez consulter la question (et les réponses qui en découlent) que j'ai postée à propos de cette même chose.

7 votes

Pour une opération 2^x, si x est un entier, le résultat est une opération de décalage. Vous pourriez donc construire le résultat en utilisant une mantisse de 2 et un exposant de x.

0 votes

@SurajJain votre commentaire est en fait une question que vous devez poster séparément.

874voto

Hans Passant Points 475940

MethodImplOptions.InternalCall

Cela signifie que la méthode est réellement implémentée dans le CLR, écrite en C++. Le compilateur juste-à-temps consulte une table avec des méthodes implémentées en interne et compile l'appel à la fonction C++ directement.

Jeter un œil au code nécessite le code source du CLR. Vous pouvez l'obtenir à partir de la distribution SSCLI20. Il a été écrit autour de la période de temps de .NET 2.0, j'ai trouvé que les implémentations de bas niveau, comme Math.Pow(), sont encore largement précises pour les versions ultérieures du CLR.

La table de recherche se trouve dans clr/src/vm/ecall.cpp. La section qui concerne Math.Pow() ressemble à cela :

FCFuncStart(gMathFuncs)
    FCIntrinsic("Sin", COMDouble::Sin, CORINFO_INTRINSIC_Sin)
    FCIntrinsic("Cos", COMDouble::Cos, CORINFO_INTRINSIC_Cos)
    FCIntrinsic("Sqrt", COMDouble::Sqrt, CORINFO_INTRINSIC_Sqrt)
    FCIntrinsic("Round", COMDouble::Round, CORINFO_INTRINSIC_Round)
    FCIntrinsicSig("Abs", &gsig_SM_Flt_RetFlt, COMDouble::AbsFlt, CORINFO_INTRINSIC_Abs)
    FCIntrinsicSig("Abs", &gsig_SM_Dbl_RetDbl, COMDouble::AbsDbl, CORINFO_INTRINSIC_Abs)
    FCFuncElement("Exp", COMDouble::Exp)
    FCFuncElement("Pow", COMDouble::Pow)
    // etc..
FCFuncEnd()

Rechercher "COMDouble" vous emmène à clr/src/classlibnative/float/comfloat.cpp. Je vous épargne le code, jetez-y un coup d'œil par vous-même. Fondamentalement, il vérifie les cas particuliers, puis appelle la version CRT de pow().

Le seul autre détail d'implémentation intéressant est le macro FCIntrinsic dans la table. C'est un indice que le jitter peut implémenter la fonction comme un intrinsèque. En d'autres termes, substituer l'appel de fonction par une instruction de code machine en virgule flottante. Ce n'est pas le cas pour Pow(), il n'y a pas d'instruction FPU pour cela. Mais certainement pour les autres opérations simples. À noter que cela peut rendre les mathématiques en virgule flottante en C# sensiblement plus rapides que le même code en C++, consultez cette réponse pour connaître la raison.

En passant, le code source pour le CRT est également disponible si vous avez la version complète du répertoire vc/crt/src de Visual Studio. Vous atteindrez cependant une impasse sur pow(), Microsoft a acheté ce code à Intel. Faire un meilleur travail que les ingénieurs d'Intel est peu probable. Bien que l'identité de mon livre de lycée était deux fois plus rapide lorsque j'ai essayé :

public static double FasterPow(double x, double y) {
    return Math.Exp(y * Math.Log(x));
}

Mais ce n'est pas un véritable substitut car cela accumule des erreurs à partir de 3 opérations en virgule flottante et ne traite pas les problèmes de domaine bizarres que Pow() a. Comme 0^0 et -Infinity élevé à n'importe quelle puissance.

452 votes

Grande réponse, StackOverflow a besoin de plus de ce genre de choses, au lieu de "Pourquoi voudriez-vous savoir cela ?" qui arrive beaucoup trop souvent.

2 votes

D'accord avec Tom W. Réponse fantastique. J'ai appris plus que ce à quoi je m'attendais. De manière intéressante, j'ai atteint 1000 points en marquant votre publication comme "Répondu". Merci encore une fois.

0 votes

@Hans Je ne suis pas sûr de pouvoir suivre le dernier paragraphe : Si je regarde dans crt/src/math.h, je trouve effectivement _Pow_int (~ ligne 493 sur mon installation vs2010) qui semble du moins être utilisé pour tous les appels à pow. Et cela semble être l'implémentation évidente avec décalage et multiplication. Est-ce que je rate quelque chose ici ? (J'imagine qu'une implémentation optimisée d'Intel utiliserait une sorte de sorcellerie SSE ? Il semble que nous pourrions le paralléliser un peu, mais je ne suis pas sûr que ce serait une amélioration)

115voto

Michael Graczyk Points 3469

La réponse de Hans Passant est excellente, mais je voudrais ajouter que si b est un entier, alors a^b peut être calculé de manière très efficace avec une décomposition binaire. Voici une version modifiée tirée de Hacker's Delight de Henry Warren :

public static int iexp(int a, uint b) {
    int y = 1;

    while(true) {
        if ((b & 1) != 0) y = a*y;
        b = b >> 1;
        if (b == 0) return y;
        a *= a;
    }    
}

Il note que cette opération est optimale (effectue le nombre minimum d'opérations arithmétiques ou logiques) pour tous les b < 15. De plus, il n'existe aucune solution connue au problème général de trouver une séquence optimale de facteurs pour calculer a^b pour tout b autre qu'une recherche exhaustive. C'est un problème NP-Difficile. Donc en gros, cela signifie que la décomposition binaire est aussi bien que possible.

12 votes

Cet algorithme (carré et multiplier) s'applique également si a est un nombre à virgule flottante.

16 votes

En pratique, il est possible de faire beaucoup mieux que la méthode native carré-et-multiplication. Par exemple, préparer des tables de recherche pour de petits exposants afin de carré plusieurs fois avant de multiplier, ou construire des chaînes d'additions carrées optimisées pour des exposants fixes. Ce genre de problème est essentiel pour les algorithmes cryptographiques importants, donc il y a eu beaucoup de travail pour l'optimiser. La difficulté NP concerne uniquement les asymptotes dans le pire des cas, nous pouvons souvent produire des solutions optimales ou presque optimales pour les instances du problème rencontrées en pratique.

0 votes

Le texte ne mentionne pas que a est un entier, mais le code le fait. En conséquence, je m'interroge sur la précision du résultat de la "très efficace" computation du texte.

71voto

dasblinkenlight Points 264350

Si la version C librement disponible de pow constitue un indicatif, cela ne ressemble en rien à ce à quoi vous vous attendez. Il ne vous sera pas d'une grande aide de trouver la version .NET, car le problème que vous résolvez (c'est-à-dire celui avec des entiers) est de loin plus simple, et peut être résolu en quelques lignes de code C# avec l'algorithme d'exponentiation par carrés.

0 votes

Merci pour votre réponse. Le premier lien m'a surpris car je ne m'attendais pas à une telle mise en œuvre technique massive de la fonction Pow(). Bien que la réponse de Hans Passant confirme que c'est la même chose dans le monde .Net aussi. Je pense que je peux résoudre le problème en cours en utilisant certaines des techniques répertoriées dans le lien de l'algorithme de mise au carré. Merci encore.

3 votes

Je ne crois pas que ce code soit efficace. 30 variables locales devraient saturer tous les registres. Je suppose seulement que c'est la version ARM, mais sur x86, 30 variables locales dans la méthode sont géniales.

1voto

Daniel Bahmani Points 443

En parcourant les réponses, j'ai beaucoup appris sur les calculs en arrière-plan : J'ai essayé quelques solutions sur une plateforme de codage qui a une couverture de tests extensive, et j'ai trouvé un moyen très efficace de le faire (Solution 3) :

public double MyPow(double x, int n) {
    double res = 1;
    /* Solution 1 : itératif : TLE (Dépassement de la limite de temps)
    double res = 1;
    var len = n > 0 ? n : -n;
    for(var i = 0; i < len; ++i)
        res *= x;   

    return n > 0 ? res : 1 / res; 
    */

    /* Solution 2 : récursif => dépassement de pile
    if(x == 0) return n > 0 ? 0 : 1 / x;
    if(n == 1) return x;

    return n > 0 ? x * MyPow(x, n - 1) : (1/x) * MyPow(1/x, -n); 
    */

    //Solution 3 :
    if (n == 0) return 1;

    var half = MyPow(x, n / 2);
    if (n % 2 == 0) 
        return half * half;
    else if (n > 0) 
        return half * half * x;
    else 
        return half * half / x;

    /* Solution 4 : à l'aide des bits => TLE (Dépassement de la limite de temps)
    var b = n > 0 ? n : -n;        
    while(true) {
        if ((b & 1) != 0) 
            res *= x;

        b = b >> 1;

        if (b == 0) break;

        x *= x;
    }   
    return n > 0 ? res : 1 / res; 
    */
}

0voto

Valera Points 77

Réponse acceptée sur Leetcode:

public class Solution {
    public double MyPow(double x, int n) {
        if(n==0) return 1;

        long abs = Math.Abs((long)n);

        var result = pow(x, abs);

        return n > 0 ? result : 1/result;
    }

    double pow(double x, long n){
        if(n == 1) return x;

        var result = pow(x, n/2);
        result = result * result * (n%2 == 1? x : 1);
        return result;
    }
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X