How is Math.Pow() implemented in .NET Framework?
我正在寻找一种计算ab的有效方法(比如a = 2和b = 50)。为了开始工作,我决定看看Math.Pow()函数的实现。但在.NET Reflector中,我发现的是:
1 2
| [MethodImpl(MethodImplOptions.InternalCall), SecuritySafeCritical]
public static extern double Pow(double x, double y); |
当我调用Math.Pow()函数时,我可以看到哪些资源是内部正在发生的事情?
- 作为一个参考,如果你对整个InternalCall和一个extern修饰语感到困惑(因为它们似乎有冲突),请看我发表的关于这件事的问题(以及由此产生的答案)。
- 对于2^x操作,如果x为整数,则结果为移位操作。所以你可以用EDOCX1的尾数(4)和EDOCX1的指数(3)来构造结果。
- @你的评论实际上是一个你需要单独发表的问题。
- @我同意你的看法。我不是主持人,所以我不能在这里做太多。也许可以在meta.stackoverflow.com上提出投票否决问题。
MethodImplOptions.InternalCall
这意味着该方法实际上是在CLR中实现的,用C++编写。准时编译器使用内部实现的方法咨询一个表,并直接编译调用C++函数。
查看代码需要clr的源代码。你可以从SSCLI20发行版上得到。它是围绕.NET 2.0时间框架编写的,我发现低级的实现,如Math.Pow(),对于更高版本的clr仍然大体上是准确的。
查找表位于clr/src/vm/ecall.cpp中。与Math.Pow()相关的部分如下:
1 2 3 4 5 6 7 8 9 10 11
| 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/float/comfloat.cpp。我不给你密码了,你自己看看。它基本上检查角落的情况,然后调用CRT版本的pow()。
唯一有趣的其他实现细节是表中的fcinherin宏。这是一个提示,即抖动可能将函数作为内部函数实现。换句话说,用浮点机器代码指令替换函数调用。pow()不是这样的,它没有FPU指令。但对于其他简单的操作当然是如此。值得注意的是,这可以使C语言中的浮点运算基本上比C++中的相同代码快,检查这个答案的原因。
顺便说一下,如果您有完整版本的Visual Studio VC/CRT/SRC目录,那么CRT的源代码也可用。不过,微软从英特尔那里购买了这段代码,这对pow()来说是个打击。比英特尔工程师做得更好是不太可能的。尽管我读高中时的身份是我读高中时的两倍:
1 2 3
| public static double FasterPow(double x, double y) {
return Math.Exp(y * Math.Log(x));
} |
但不是一个真正的替代,因为它从3个浮点运算中积累了错误,并且不处理pow()所具有的奇怪域问题。就像0^0和-infinity被提升到任何幂。
- 很好的答案是,stackoverflow需要更多这样的东西,而不是"你为什么想知道?"这种情况经常发生。
- 同意汤姆·W的回答。太好了。我学到的比我期望的要多。有趣的是,我通过把你的帖子标记为"答案"达到了1000分。再次感谢。
- @汉斯,我不确定我是否能遵循最后一段:如果我查看crt/src/math.h,我确实找到了_Pow_int(在我的vs2010安装中有~493行),在我看来,它至少用于所有的pow调用。这似乎是明显的转变和成倍的实施。我是不是错过了什么?(我假设优化的英特尔实现将使用某种SSE魔法?似乎我们可以将其并行处理一点,但不确定这是否会有所改进)
- @voo—这是一个模板专门化的pow()调用传递整数幂。就像pow(x,2)=x*x。是的,Intel版本使用非常复杂的SSE2机器代码。
- @汉斯·厄普斯,我完全错过了权力是双重的部分。兆。
- @蓝色-我不知道,只是取笑英特尔的工程师。我的高中书确实有一个问题,那就是如何提高负积分的威力。pow(x,-2)是完全可计算的,pow(x,-2.1)是未定义的。域名问题是个可恶的问题。
- @blueraja dannyplughoeft:我们花费了大量精力来确保浮点运算尽可能接近正确的四舍五入值。众所周知,pow很难准确地实现,它是一个超越函数(参见表生成器的困境)。有了一个完整的力量就容易多了。
- @汉斯·帕桑特:为什么要定义pow(x,-2.1)?数学上,pow在所有x和y的任何地方都有定义。对于负x和非整数y,你会得到复数。
- @未定义jules pow(0,0)。
- @浮雕:对,这是唯一的情况。虽然一些数学家把它定义为1或0,但是pow在(0,0)处有一个基本的不连续性,因此没有唯一的方法来修补它(所以最好的方法可能是根本不修补它)。
- @Jules复数刚加入到4.0框架中,msdn.microsoft.com/en-us/library/…。它现在定义了POW(复杂、双重)和POW(复杂、复杂)。
- @是吗?五年前,当我在一个桌面应用程序中编写用于DIY平铺的地理空间功能时,这真是太方便了。谢天谢地,谷歌在我发疯之前就把它废弃了。茜茜。
- @Hanspasant-我想你的意思是,对于x的负值,Pow(x, -2.1)是未定义的(或复杂的),对吗?除非x为0,否则指数上的符号不会使其多少可计算。
- 到了罗斯林,这仍然是真的吗?我看到Github上的corecrl仍然显示这是一个内部调用:github.com/dotnet/corecrl/blob/…
- 看看这个问题,注意它是相同的。
- 我认为这是实际的C++ CLR实现(至少对于COCECLR)GITHUB.COM/DOTNET/COCECLR/BLB//Helip;
- github.com/dotnet/corecrl/blob/…
汉斯·帕桑特的回答很好,但我想补充一点,如果b是一个整数,那么a^b可以用二进制分解非常有效地计算出来。以下是亨利·沃伦的《黑客之乐》的修改版本:
1 2 3 4 5 6 7 8 9 10
| 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难题。所以基本上这意味着二元分解是尽可能好的。
- 如果a是浮点数,则此算法(平方和乘法)也适用。
- 在实践中,有可能做得比原生平方和乘法更好一些。例如,为小指数准备查找表,以便您可以平方多次,然后才进行乘法,或者为固定指数构建优化的平方加法链。这类问题是重要密码算法的一个组成部分,因此对其进行了大量的优化工作。NP硬度仅是关于最坏情况的渐近性,对于实际中出现的问题,我们通常可以得到最优或近似最优解。
- 文本没有提到a是一个整数,但代码是这样的。因此,我怀疑文本"非常有效"计算结果的准确性。
如果免费提供的pow的C版本有任何指示,它看起来不像您期望的那样。找到.NET版本对您没有多大帮助,因为您要解决的问题(即具有整数的问题)是更简单的数量级,并且可以通过平方算法的求幂在几行C代码中解决。
- 谢谢你的回答。第一个链接让我吃惊,因为我没想到POW()函数会有如此大规模的技术实现。虽然HansPassant的回答也证实了.NET世界中的情况是一样的。我想我可以利用平方算法链接中列出的一些技术来解决手头的问题。再次感谢。
- 我不相信这个代码是有效的。30个局部变量只会碰到所有寄存器。我只认为它是ARM版本,但是在x86 30中,方法中的局部变量非常棒。