如何在 C 中计算 pow()?
How is pow() calculated in C?
我们教授说你不能用pow()
计算ab如果a<0因为pow()
用自然对数来计算它(a b=eb ln a) 并且由于负数未定义,因此无法计算。我试过了,只要 b 是整数,它就可以工作。
我搜索了 math.h
和更多文件,但无法找到该函数的定义方式以及它用于计算的内容。我也尝试在互联网上搜索,但没有成功。 Stack Overflow 右边here and here (for C#) 上也有类似的问题。 (最后一个不错,就是没找到源码)
所以问题是 pow()
实际上是如何在 C 中计算的?为什么它 return 当基数是有限且负的且指数是有限且非整数时出现域错误?
假设一个x86系列处理器,pow
相当于
double pow(double base, double exp)
{
return exp2(exp * log2(base));
}
其中 exp2
和 log2
是 CPU 原语,用于以 2 为底的指数和对数运算。
不同的CPU本质上有不同的实现。
理论上如果你没有pow
你可以写:
double pow(double base, double exponent)
{
return exp(exponent * log(base));
}
但是由于累积舍入,这会比原生版本失去精度。
Dietrich Epp 透露我错过了很多特殊情况。尽管如此,关于四舍五入我还是有话要说的。
第二个问题(为什么 return 是一个域错误)已经在评论中提到了,但为了完整性添加:pow
需要两个实数,return 是一个实数数字。对负数应用有理指数会使您脱离实数域,进入复数域,而该函数的结果(double)无法表示。
如果您对实际实施感到好奇,那么,有很多实施,这取决于许多因素,例如体系结构和优化级别。很难找到一个容易阅读的,但是 FDLIBM (Freely Distributable LIBM) has one which has at least has a good explanation in the comments:
/* __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
函数在实践中如何实现感到好奇,您可以查看源代码。在不熟悉的(和大的)代码库中搜索以找到您要查找的部分是一种 "knack",并且最好进行一些练习。
C 库的一个实现是 glibc,它在 GitHub 上有镜像。我没有找到官方镜像,但非官方镜像位于 https://github.com/lattera/glibc
我们首先看一下 math/w_pow.c
file which has a promising name. It contains a function __pow
which calls __ieee754_pow
, which we can find in 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
,pow
的 glibc 版本将计算 pow(-x,y)
,然后如果 y
是奇数。
这不是做事的唯一方法,但我猜这对许多实现来说都是通用的。你可以看到 pow
充满了特殊情况。这在库数学函数中很常见,它们应该可以正确处理非正规数和无穷大等不友好的输入。
pow
函数特别难读,因为它是经过大量优化的代码,对浮点数进行位运算。
C 标准
C 标准 (n1548 §7.12.7.4) 对 pow
有这样的说法:
A domain error occurs if x is finite and negative and y is finite and not an integer value.
因此,根据 C 标准,否定 x
应该 有效。
还有附录 F 的问题,它对 pow
如何在 IEEE-754 / IEC-60559 系统上工作给出了更严格的限制。
pow
对负数有效。当基数为负且指数不是整数时,它不起作用。
形式为ax/y的数实际上涉及x的y次方根。例如,当您尝试计算 a1/2 时,您实际上是在寻找 a.
的平方根
那么如果你有一个负底数和一个非整数指数会怎样?你得到一个负数的第 y 个根,它产生一个复杂的非实数。 pow()
不适用于复数,因此它可能 return NaN。
我们教授说你不能用pow()
计算ab如果a<0因为pow()
用自然对数来计算它(a b=eb ln a) 并且由于负数未定义,因此无法计算。我试过了,只要 b 是整数,它就可以工作。
我搜索了 math.h
和更多文件,但无法找到该函数的定义方式以及它用于计算的内容。我也尝试在互联网上搜索,但没有成功。 Stack Overflow 右边here and here (for C#) 上也有类似的问题。 (最后一个不错,就是没找到源码)
所以问题是 pow()
实际上是如何在 C 中计算的?为什么它 return 当基数是有限且负的且指数是有限且非整数时出现域错误?
假设一个x86系列处理器,pow
相当于
double pow(double base, double exp)
{
return exp2(exp * log2(base));
}
其中 exp2
和 log2
是 CPU 原语,用于以 2 为底的指数和对数运算。
不同的CPU本质上有不同的实现。
理论上如果你没有pow
你可以写:
double pow(double base, double exponent)
{
return exp(exponent * log(base));
}
但是由于累积舍入,这会比原生版本失去精度。
Dietrich Epp 透露我错过了很多特殊情况。尽管如此,关于四舍五入我还是有话要说的。
第二个问题(为什么 return 是一个域错误)已经在评论中提到了,但为了完整性添加:pow
需要两个实数,return 是一个实数数字。对负数应用有理指数会使您脱离实数域,进入复数域,而该函数的结果(double)无法表示。
如果您对实际实施感到好奇,那么,有很多实施,这取决于许多因素,例如体系结构和优化级别。很难找到一个容易阅读的,但是 FDLIBM (Freely Distributable LIBM) has one which has at least has a good explanation in the comments:
/* __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
函数在实践中如何实现感到好奇,您可以查看源代码。在不熟悉的(和大的)代码库中搜索以找到您要查找的部分是一种 "knack",并且最好进行一些练习。
C 库的一个实现是 glibc,它在 GitHub 上有镜像。我没有找到官方镜像,但非官方镜像位于 https://github.com/lattera/glibc
我们首先看一下 math/w_pow.c
file which has a promising name. It contains a function __pow
which calls __ieee754_pow
, which we can find in 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
,pow
的 glibc 版本将计算 pow(-x,y)
,然后如果 y
是奇数。
这不是做事的唯一方法,但我猜这对许多实现来说都是通用的。你可以看到 pow
充满了特殊情况。这在库数学函数中很常见,它们应该可以正确处理非正规数和无穷大等不友好的输入。
pow
函数特别难读,因为它是经过大量优化的代码,对浮点数进行位运算。
C 标准
C 标准 (n1548 §7.12.7.4) 对 pow
有这样的说法:
A domain error occurs if x is finite and negative and y is finite and not an integer value.
因此,根据 C 标准,否定 x
应该 有效。
还有附录 F 的问题,它对 pow
如何在 IEEE-754 / IEC-60559 系统上工作给出了更严格的限制。
pow
对负数有效。当基数为负且指数不是整数时,它不起作用。
形式为ax/y的数实际上涉及x的y次方根。例如,当您尝试计算 a1/2 时,您实际上是在寻找 a.
的平方根那么如果你有一个负底数和一个非整数指数会怎样?你得到一个负数的第 y 个根,它产生一个复杂的非实数。 pow()
不适用于复数,因此它可能 return NaN。