为什么标准的 C + + 库中没有‘ int pow (int base,int 幂)’?

我觉得我一定是找不到了。除了 floatdouble之外,C + + pow函数没有实现任何“ power”函数,这是否有什么原因呢?

我知道实现是琐碎的,我只是觉得我所做的工作应该在一个标准库中。一个健壮的幂函数(即以某种一致的、明确的方式处理溢出)并不好写。

84556 次浏览

因为无论如何都不可能在 int 中表示所有的整数幂:

>>> print 2**-4
0.0625

对于任何固定宽度的整数类型,几乎所有可能的输入对都会溢出该类型。如果一个函数的大部分可能的输入都没有得到有用的结果,那么标准化这个函数有什么用呢?

为了使函数有用,您几乎需要一个大整数类型,而且大多数大整数库都提供了该函数。


编辑: 在对问题的评论中,static _ rtti 写道: “大多数输入导致它溢出?Exp 和 double pow 也是如此,我没看到有人抱怨。”这是错误的。

让我们把 exp放在一边,因为那不是重点(尽管它实际上会使我的情况更强) ,而是关注于 double pow(double x, double y)。对于(x,y)对的哪一部分,这个函数做一些有用的事情(即,不仅仅是溢出或下溢) ?

我实际上只关注 pow有意义的一小部分输入对,因为这足以证明我的观点: 如果 x 为正,| y | < = 1,那么 pow不会溢出或下溢。这包括近四分之一的所有浮点对(正好一半的非 NaN 浮点数是正的,只有不到一半的非 NaN 浮点数小于1)。显然,还有其他输入对的 很多,对它们 pow产生有用的结果,但是我们已经确定它至少占所有输入的四分之一。

现在让我们来看一个固定宽度(即非 bignum)的整数幂函数。对于什么部分的输入,它不会简单地溢出?为了使有意义的输入对的数目最大化,基数应该是有符号的,而指数应该是无符号的。假设基数和指数都是 n位宽。我们可以很容易地得到有意义的输入部分的界限:

  • 如果指数为0或1,则任何基数都是有意义的。
  • 如果指数大于等于2,则任何大于2 ^ (n/2)的基数都不会产生有意义的结果。

因此,在2 ^ (2n)输入对中,少于2 ^ (n + 1) + 2 ^ (3n/2)产生有意义的结果。如果我们看看最常见的用法,32位整数,这意味着大约1/1000的输入对不会简单地溢出。

也许是因为处理器的 ALU 没有为整数实现这样的函数,但是存在这样的 FPU 指令(正如 Stephen 指出的,它实际上是一对)。所以实际上转换为 double,使用 double 调用 pow,然后测试溢出和转换回来,比使用整数算法实现要快。

(一方面,对数减少了乘法的能力,但对于大多数输入,整数的对数失去了很多精度)

Stephen 是对的,在现代处理器上,这已经不再正确,但是数学函数被选择时的 C 标准(C + + 只是使用了 C 函数)现在已经有20年的历史了吧?

C++11开始,特殊情况被添加到电源功能套件(和其他功能)中。在列出所有 float/double/long double重载之后,C++11 [c.math] /11指出:

此外,还应有足够的额外超载,以确保 < strong > 如果对应于 double参数的任何参数具有 double类型或整数类型,那么对应于 double参数的所有参数将有效地转换为 double

因此,基本上,整数参数将升级为双精度参数来执行操作。


C++11之前(也就是问你问题的时候) ,不存在整数重载。

因为在 CC++创建的时候,我既没有和它们的创建者密切联系(虽然我 相当老) ,也没有参与创建标准的 ANSI/ISO 委员会,这是我的观点。我愿意认为这是 消息灵通的观点,但是,正如我妻子会告诉你的(经常而且不需要太多的鼓励) ,我以前错过: -)

不管怎样,假设如下。

疑犯,原来的前 ANSI C没有这个功能的原因是因为它是完全不必要的。首先,已经有一种非常好的方法来处理整数幂(使用双精度变量,然后简单地转换回整数,在转换之前检查整数溢出和下溢)。

其次,您必须记住的另一件事是,C的最初意图是作为一种 系统编程语言,而浮点数在这个领域是否可取是值得怀疑的。

由于它最初的用例之一是编写 UNIX 代码,因此浮点数几乎没有用处。基于 C 语言的 BCPL 也不需要能量(它根本没有浮点数)。

另外,积分幂运算符可能是二进制运算符,而不是库调用。使用 x = add (y, z)但是使用 x = y + z(用正确的语言的一部分而不是库)不添加两个整数。

第三,由于整合能力的实现相对来说是微不足道的,几乎可以肯定的是,语言的开发人员会更好地利用他们的时间来提供更有用的东西(参见下面关于机会成本的评论)。

这也与最初的 C++有关。由于最初的实现实际上只是一个生成 C代码的翻译器,因此它继承了 C的许多属性。它最初的意图是 C-with-class,而不是 C-with-class-plus-a-little-bit-more-data-stuff。

至于为什么在 C++11之前它从未被添加到标准中,你必须记住,标准制定机构有具体的指导方针要遵循。例如,ANSI C专门负责编纂现有的实践,没有创建一种新的语言。否则,他们可能会发疯,把艾达交给我们: -)

该标准的后续迭代也有具体的指导方针,可以在基本原理文档中找到(关于为什么委员会做出某些决定的基本原理,而不是语言本身的基本原理)。

例如,C99基本原理文件明确提出了 C89的两项指导原则,限制了可以增加的内容:

  • 保持语言简洁。
  • 只提供执行操作的一种方法。

指导方针(不一定是那些 具体点的)是为个别工作组制定的,因此限制 C++委员会(和所有其他 ISO 组)以及。

此外,标准制定机构意识到,他们做出的每一个决定都有一个 机会成本(一个经济术语,意思是你在做决定时必须放弃的东西)。例如,购买那台价值10,000美元的超级游戏机的机会成本是与你的另一半在大约六个月的时间里保持友好关系(或者可能是 所有关系)。

Eric Gunnerson 在他的 -100分解释中很好地解释了为什么微软的产品并不总是添加这些东西——基本上一个功能从100点开始,所以它必须增加相当多的价值才能被考虑。

换句话说,你是愿意使用一个完整的电源运算符(老实说,任何一个半吊子的程序员都可以在10分钟内完成) ,还是愿意将多线程添加到标准中?对于我自己来说,我更喜欢后者,而不必为 UNIX 和 Windows 下的不同实现而烦恼。

我还希望看到成千上万的标准库收藏(散列、 btree、红黑树、字典、任意地图等等) ,但正如其基本原理所述:

标准是实现者和程序员之间的协议。

标准体系中的实现者数量远远超过程序员的数量(或者至少那些不理解机会成本的程序员)。如果所有这些东西都被添加进来,那么下一个标准 C++将是 C++215x,并且很可能在300年后由编译器开发人员完全实现。

无论如何,这就是我对这件事的(相当多的)想法。如果投票是以数量而不是质量为基础的话,我很快就会把其他人都打得落花流水。感谢收听: -)

C + + 没有额外重载的一个原因是为了与 C 兼容。

C + + 98有类似于 double pow(double, int)的函数,但是在 C + + 11中已经删除了这些函数,因为 C99没有包含它们。

Http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2011/n3286.html#550

得到一个稍微更精确的结果也意味着得到一个稍微 与众不同的结果。

这真是个有趣的问题。我在讨论中没有发现的一个参数是这些参数没有明显的返回值。让我们计算一下假设的 int pow_int(int, int)函数可能失败的方式。

  1. 溢出
  2. 结果未定义的 pow_int(0,0)
  3. 结果不能表示为 pow_int(2,-1)

该函数至少有两种失效模式。整数不能表示这些值,在这些情况下,函数的行为需要由标准来定义——程序员需要知道函数是如何处理这些情况的。

总的来说,省略这个函数似乎是唯一明智的选择。程序员可以使用浮点版本代替所有可用的错误报告。

一个很简单的原因:

5^-2 = 1/25

STL 库中的一切都基于最精确、最健壮的东西。当然,int 将返回0(从1/25开始) ,但这将是一个不准确的答案。

我同意,有时候确实很奇怪。

简短的回答:

对于 时间表演来说,将 pow(x, n)专门化到 n是一个自然数通常是有用的。但是标准库的通用 pow()仍然可以很好地工作(出人意料!) ,因此在标准 C 库中包含尽可能少的代码是非常关键的,这样它就可以尽可能地便于移植和实现。另一方面,这并不能阻止它进入 C++标准程式库或 STL,我很确定没有人打算在某种嵌入式平台上使用它。

现在,长话短说。

在许多情况下,通过将 n专门化为一个自然数,可以使 pow(x, n)变得更快。对于几乎所有我编写的程序,我都必须使用我自己的这个函数的实现(但是我用 C 语言编写了很多数学程序)。专业的操作可以在 O(log(n))时间内完成,但当 n较小时,一个更简单的线性版本可以更快。以下是两者的实现:


// Computes x^n, where n is a natural number.
double pown(double x, unsigned n)
{
double y = 1;
// n = 2*d + r. x^n = (x^2)^d * x^r.
unsigned d = n >> 1;
unsigned r = n & 1;
double x_2_d = d == 0? 1 : pown(x*x, d);
double x_r = r == 0? 1 : x;
return x_2_d*x_r;
}
// The linear implementation.
double pown_l(double x, unsigned n)
{
double y = 1;
for (unsigned i = 0; i < n; i++)
y *= x;
return y;
}

(我将 x和返回值保留为双精度,因为 pow(double x, unsigned n)的结果将与 pow(double, double)的结果一样适合双精度。)

(是的,pown是递归的,但是打破堆栈是绝对不可能的,因为最大堆栈大小将大致等于 log_2(n)n是一个整数。如果 n是一个64位整数,则堆栈的最大大小为64。没有硬件具有如此极端的内存限制,除了一些带有硬件堆栈的可疑 PIC,它们只能深入3到8个函数调用。)

至于性能,你会惊讶于什么是一个花园品种 pow(double, double)的能力。我在使用了5年的 IBM Thinkpad 上测试了1亿次迭代,其中 x等于迭代次数,n等于10。在这种情况下,pown_l赢了。Glibc pow()用了12.0用户秒,pown用了7.4用户秒,而 pown_l只用了6.5用户秒。所以这并不奇怪。我们或多或少预料到了这一点。

然后,我让 x保持恒定(我将它设置为2.5) ,并且将 n从0循环到19循环1亿次。这一次,出乎意料的是,glibc pow以压倒性的优势获胜!它只需要2.0用户秒。我的 pown用了9.6秒 pown_l用了12.2秒。这里发生了什么?我又做了一个测试。

我做了和上面一样的事情,只是 x等于100万。这一次,pown以9.6秒的成绩获胜,pown_l以12.2秒的成绩获胜,而 glibc Pow 以16.3秒的成绩获胜。现在,一切都清楚了!在 x较低时,glibc pow的表现优于三种,但在 x较高时表现最差。x高时,pown_ln低时表现最好,pownx高时表现最好。

因此,这里有三种不同的算法,每种算法都能够在正确的情况下比其他算法表现得更好。因此,最终,使用哪个版本最有可能取决于您计划如何使用 pow,但使用正确的版本 是值得的,拥有所有的版本是很好的。事实上,你甚至可以用这样的函数自动选择算法:

double pown_auto(double x, unsigned n, double x_expected, unsigned n_expected) {
if (x_expected < x_threshold)
return pow(x, n);
if (n_expected < n_threshold)
return pown_l(x, n);
return pown(x, n);
}

只要 x_expectedn_expected是在编译时确定的常量,加上可能的其他一些注意事项,一个称职的优化编译器将自动删除整个 pown_auto函数调用,并用三种算法中的适当选择替换它。(现在,如果你真的要尝试 使用这个,你可能不得不玩弄它一下,因为我没有完全尝试 编译中我写了上面。;))

另一方面,glibc pow 确实有用和 glibc 已经足够大了。C 标准被认为是可移植的,包括各种 嵌入式设备(事实上,各地的嵌入式开发人员普遍认为 glibc 对他们来说已经太大了) ,如果每个简单的数学函数都需要包含 也许吧可以使用的所有替代算法,那么它就不可移植。所以,这就是为什么它不在 C 标准中。

脚注: 在性能测试中,我给我的函数设置了相对宽泛的优化标志(-s -O2) ,这些标志可能与在我的系统(archlinux)上编译 glibc 时使用的标志相当,甚至更差,所以结果可能是公平的。要进行更严格的测试,我必须自己编译 glibc,而我 真的不想这样做。我过去常用 Gentoo,所以我记得它需要多长时间,即使任务是 ABc2。结果对我来说已经足够确凿(或者说不确凿)了。你当然可以自己来。

额外奖励: 如果需要精确的整数输出,那么对所有整数的 pow(x, n)专门化就是 工具,这种情况确实会发生。考虑为具有 p ^ N 个元素的 N 维数组分配内存。即使 p ^ N 减去1,也会导致可能随机发生的段错误。

世界在不断变化,编程语言也在不断变化。C 小数的第四部分1为 <math.h>增加了一些功能。这个问题可能涉及这些功能的两个系列:

  • pown函数接受一个浮点数和一个 intmax_t指数。
  • powr函数接受两个浮点数(xy) ,并用公式 exp(y*log(x))计算 x的幂 y

似乎标准人员最终认为这些特性足够有用,可以集成到标准库中。然而,合理的解释是, ISO/IEC/IEEE 60559:2011 标准推荐这些函数用于二进制和十进制浮点数。我不能肯定地说在 C89的时候遵循了什么样的“标准”,但是 <math.h>的未来发展可能会受到 ISO/IEC/IEEE 60559标准未来发展的很大影响。

请注意,十进制 TR 的第四部分不会包含在 C2x (下一个主要的 C 修订版)中,并且可能会在以后作为一个可选特性包含在内。据我所知,在未来的 C + + 修订版中,没有任何意图包含 TR 的这一部分。


1您可以找到一些在制作中的文档 给你

事实上,是的。

因为 C + + 11有一个模板化的 pow(int, int)实现——-甚至更一般的情况,参见(7) Http://en.cppreference.com/w/cpp/numeric/math/pow


编辑: 纯粹主义者可能认为这是不正确的,因为实际上有“促进”类型使用。不管怎样,我们都会得到一个正确的 int结果,或者一个错误,关于 int参数。

下面是一个非常简单的 pow ()的 O (log (n))实现,它适用于任何数值类型 包括整数:

template<typename T>
static constexpr inline T pown(T x, unsigned p) {
T result = 1;


while (p) {
if (p & 0x1) {
result *= x;
}
x *= x;
p >>= 1;
}


return result;
}

它比 enigmaticPhysical 的 O (log (n))实现更好,因为它不使用递归。

它也几乎总是比他的线性实现快(只要 p > ~ 3) ,因为:

  • 不需要任何额外的内存
  • 每个循环只执行大约1.5倍的操作
  • 它每个循环只能执行约1.25倍的内存更新