C语言中的pow()如何计算?

11
我们的教授说,如果a<0,使用pow()无法计算ab,因为pow()使用自然对数来计算它(ab=eb ln a),由于负数未定义,所以无法计算。我尝试了一下,只要b是整数,就可以正常工作。
我已经在math.h和其他文件中搜索过,但无法找到该函数的定义方式以及它用于计算的内容。我也试图在互联网上搜索,但没有任何成功。类似的问题在Stack Overflow上有这里这里(针对C#)。 (最后一个很好,但我无法找到源代码。)
所以问题是,在C中如何实际计算pow()?为什么当底数为有限且为负数,指数为有限且非整数时会返回域错误?

3
pow函数的精确计算方式是一个较长的故事,但它无法在底数为负且指数为非整数时进行计算,因为结果将会是一个复数,而该复数不能由pow返回的double类型表示。不过,可以使用cpow函数来处理这种情况。 - Wintermute
6
C99 7.12.7.4 (2) 规定:“pow函数计算x的y次方。如果x是有限负数且y是有限非整数值,则会发生域错误”,因此我认为它不取决于实现。只要b是无穷大或整数,a可以是负数。 - Wintermute
1
我已经搜索了math.h和其他文件,但是无法找到该函数的定义 - 头文件没有定义(inline函数是一种特殊情况)。但我强烈怀疑您找不到实现!至少有一个FOSS C库可用。像“c pow implementation”这样的谷歌搜索词真的那么不常见吗? - too honest for this site
1
没有经过优化的编译将得到实际 C 库中的结果。使用像 -O3 这样的标志,结果可能是经过优化的 SSE/AVX 实现,可以通过反汇编二进制文件进行调查。 - Kh40tiK
谢谢@Wintermute。现在你说它是一个复数,我更加好奇它是如何工作的。 - Szymon
显示剩余11条评论
4个回答

19

如果你想知道实现pow函数的具体方法,可以查看源代码。在陌生(且庞大)的代码库中查找所需部分有一定技巧,因此最好练习一下。

C库的一个实现是glibc,它在GitHub上有镜像。我没有找到官方镜像,但非官方的镜像在https://github.com/lattera/glibc

我们首先查看名为math/w_pow.c的文件,这个名称很有前途。其中包含一个名为__pow的函数,调用了__ieee754_pow,我们可以在sysdeps/ieee754/dbl-64/e_pow.c中找到它。(请记住,并非所有系统都符合IEEE-754标准,因此IEEE-754数学代码位于其自己的目录中)。

它从一些特殊情况开始:

if (y == 1.0) return x;
if (y == 2.0) return x*x;
if (y == -1.0) return 1.0/x;
if (y == 0) return 1.0;

稍微往下走,你会发现一个带有评论的分支。

/* if x<0 */

这就引出了我们接下来要谈论的内容

return (k==1)?__ieee754_pow(-x,y):-__ieee754_pow(-x,y); /* if y even or odd */

因此,对于负数x和整数y,glibc版本的pow将计算pow(-x,y),如果y为奇数,则将结果变为负数。

这不是唯一的做法,但我猜这在许多实现中都是常见的。您可以看到,pow充满了特殊情况。这在库数学函数中很常见,这些函数应该能够正确处理不友好的输入,如非规格化数和无穷大。

pow函数尤其难以阅读,因为它是经过大量优化的代码,可以对浮点数进行位操作。

C标准

C标准(n1548 §7.12.7.4)对pow有以下规定:

  

如果x有限且为负数,y是有限的且不是整数值,则会发生域错误。

因此,根据C标准,负数x 应该有效。

还有附录F的问题,它对IEEE-754 / IEC-60559系统上的pow如何工作给出了更严格的约束。


谢谢。我想我会花些时间来理解它。 在 e_pow.c 中的一个离题问题上,有这样一行代码 return y < 0 ? 1.0/0.0 : 0.0;,这让我想到为什么要返回 1.0/0.0 - Szymon
3
1.0/0.0 表示无穷大。 - Dietrich Epp

6
第二个问题(为什么它返回域错误)已经在评论中解决,但为了完整起见:pow需要两个实数并返回一个实数。将有理指数应用于负数会将您带出实数域进入复数域,而此函数的结果(一个双精度浮点数)无法表示。如果您对实际实现感到好奇,那么实现有很多因素,例如架构和优化级别。很难找到一个易于阅读的实现,但FDLIBM(Freely Distributable LIBM)有一个至少在注释中有很好解释的实现
/* __ieee754_pow(x,y) return x**y
 *
 *            n
 * Method:  Let x =  2   * (1+f)
 *  1. Compute and return log2(x) in two pieces:
 *      log2(x) = w1 + w2,
 *     where w1 has 53-24 = 29 bit trailing zeros.
 *  2. Perform y*log2(x) = n+y' by simulating muti-precision 
 *     arithmetic, where |y'|<=0.5.
 *  3. Return x**y = 2**n*exp(y'*log2)
 *
 * Special cases:
 *  1.  (anything) ** 0  is 1
 *  2.  (anything) ** 1  is itself
 *  3.  (anything) ** NAN is NAN
 *  4.  NAN ** (anything except 0) is NAN
 *  5.  +-(|x| > 1) **  +INF is +INF
 *  6.  +-(|x| > 1) **  -INF is +0
 *  7.  +-(|x| < 1) **  +INF is +0
 *  8.  +-(|x| < 1) **  -INF is +INF
 *  9.  +-1         ** +-INF is NAN
 *  10. +0 ** (+anything except 0, NAN)               is +0
 *  11. -0 ** (+anything except 0, NAN, odd integer)  is +0
 *  12. +0 ** (-anything except 0, NAN)               is +INF
 *  13. -0 ** (-anything except 0, NAN, odd integer)  is +INF
 *  14. -0 ** (odd integer) = -( +0 ** (odd integer) )
 *  15. +INF ** (+anything except 0,NAN) is +INF
 *  16. +INF ** (-anything except 0,NAN) is +0
 *  17. -INF ** (anything)  = -0 ** (-anything)
 *  18. (-anything) ** (integer) is (-1)**(integer)*(+anything**integer)
 *  19. (-anything except 0 and inf) ** (non-integer) is NAN
 *
 * Accuracy:
 *  pow(x,y) returns x**y nearly rounded. In particular
 *          pow(integer,integer)
 *  always returns the correct integer provided it is 
 *  representable.
 *
 * Constants :
 * The hexadecimal values are the intended ones for the following 
 * constants. The decimal values may be used, provided that the 
 * compiler will convert from decimal to binary accurately enough 
 * to produce the hexadecimal values shown.
 */

因此,简而言之,机制与您描述的一样,并且首先依赖于计算对数,但需要考虑许多特殊情况。

有关pow()适用的特殊情况的枚举,还请参见ISO-C99标准第F.9.4.4节(或最新的ISO-C11标准相应部分)。 - njuffa

5
假设使用x86系列处理器,“pow”相当于:
double pow(double base, double exp)
{
   return exp2(exp * log2(base));
}

其中,exp2log2是CPU原语,用于计算以2为底数的指数和对数操作。

不同的CPU天生具有不同的实现方式。

理论上,如果没有pow函数,你可以这样写:

double pow(double base, double exponent)
{
   return exp(exponent * log(base));
}

但是由于累积的舍入误差,这种方法失去了原生版本的精度。

Dietrich Epp指出我错过了很多特殊情况。尽管如此,我还是有一些关于舍入误差的话要说,这应该被允许存在。


base <= 0时,对于一半的double会失败 - 这并不是一个真正的“特殊”情况。 - chux - Reinstate Monica

2

pow对于负数也是有效的。只有当底数为负数且指数不是整数时,它才无法正常工作。

形如ax/y的数字实际上涉及到x的y次根。例如,当您尝试计算a1/2时,您实际上是在寻找a的平方根。

那么如果您有一个负底数和一个非整数指数会发生什么?您将获得一个负数的y次根,这将产生一个复杂的非实数。 pow()不能使用复数,因此它可能会返回NaN。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接