Math.Pow()是如何在.NET Framework中实现的

本文关键字:Framework NET 实现 Pow Math | 更新日期: 2023-09-27 18:20:16

我正在寻找一种有效的方法来计算ab(比如a = 2b = 50)。首先,我决定研究一下Math.Pow()函数的实现。但在.NET Reflector中,我发现的只是:

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

当我调用Math.Pow()函数时,我可以从哪些资源中看到内部发生了什么?

Math.Pow()是如何在.NET Framework中实现的

MethodImplOptions.InternalCall

这意味着该方法实际上是在用C++编写的CLR中实现的。即时编译器使用内部实现的方法查询表,并直接编译对C++函数的调用。

查看代码需要CLR的源代码。你可以从SSCLI20分布中得到。它是围绕.NET 2.0时间框架编写的,我发现像Math.Pow()这样的低级实现对于CLR的后续版本来说仍然非常准确。

查找表位于clr/src/vm/ecall.cpp中。与Math.Pow()相关的部分如下所示:

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()

正在搜索";COMDouble";带您访问clr/src/classlibnative/foat/comfloat.cpp。我会省去代码,您自己看看。它基本上检查角落案例,然后调用CRT版本的pow()

唯一感兴趣的其他实现细节是表中的FCIntrinsic宏。这是一个暗示,抖动可能会实现一个内在的功能。换句话说,用浮点机器代码指令代替函数调用。Pow()的情况并非如此,它没有FPU指令。但对于其他简单的操作来说,这是肯定的。值得注意的是,这可以使C#中的浮点运算比C++中的相同代码快得多,请检查这个答案以了解原因。

顺便说一句,如果您有Visual Studio vc/CRT/src目录的完整版本,那么CRT的源代码也可用。不过,你会在pow()上碰壁,微软从英特尔购买了该代码。不太可能比英特尔的工程师做得更好。尽管当我尝试的时候,我的高中书的识别速度是它的两倍:

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

但这并不是一个真正的替代品,因为它累积了3个浮点运算的错误,并且没有处理Pow()所具有的奇怪的域问题。就像0^0和-无限提升到任何力量。

Hans-Passant的答案很好,但我想补充一点,如果b是整数,那么通过二进制分解可以非常有效地计算a^b。这是亨利·沃伦的《黑客的喜悦》的修改版本:

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;
    }    
}

他指出,该运算对于所有b<15.对于除了广泛搜索之外的任何b,对于寻找因子的最优序列来计算a^b的一般问题,也没有已知的解决方案。这是一个NP难题。所以基本上,这意味着二进制分解是最好的。

如果免费提供的pow的C版本有任何指示,那么它看起来并不像您所期望的那样。找到.NET版本对您没有多大帮助,因为您正在解决的问题(即带整数的问题)简单了几个数量级,并且可以通过平方取幂算法在几行C#代码中解决。

通过这些答案,了解了许多关于幕后计算的知识:我在一个有大量测试覆盖案例的编码平台上尝试了一些变通方法,并找到了一种非常有效的方法(解决方案3):

public double MyPow(double x, int n) {
    double res = 1;
    /* Solution 1: iterative : TLE(Time Limit Exceeded)
    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: recursive => stackoverflow exception
    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: bitwise=> TLE(Time Limit Exceeded)
    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; 
    */
}

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;
    }
}