为啥标准 C++ 库中没有“int pow(int base, int exponent)”?

Posted

技术标签:

【中文标题】为啥标准 C++ 库中没有“int pow(int base, int exponent)”?【英文标题】:Why isn't `int pow(int base, int exponent)` in the standard C++ libraries?为什么标准 C++ 库中没有“int pow(int base, int exponent)”? 【发布时间】:2011-01-24 19:14:22 【问题描述】:

我觉得我一定是找不到它。除了floats 和doubles 之外,C++ pow 函数是否没有实现“power”函数?

我知道实现是微不足道的,我只是觉得我正在做应该在标准库中的工作。强大的幂函数(即以某种一致、明确的方式处理溢出)编写起来并不有趣。

【问题讨论】:

这是一个很好的问题,我认为答案没有多大意义。负指数不起作用?将无符号整数作为指数。大多数输入导致它溢出? exp 和 double pow 也是如此,我没有看到有人抱怨。那么为什么这个功能不是标准的呢? @static_rtti:“exp 和 double pow 也是如此”是完全错误的。我会在我的回答中详细说明。 标准 C++ 库从 C++11 开始就有 double pow(int base, int exponent)(§26.8[c.math]/11 要点 2) 你需要在“实现是微不足道的”和“写起来不好玩”之间下定决心。 【参考方案1】:

截至C++11,特殊情况已添加到幂函数(和其他)套件中。 C++11 [c.math] /11 状态,在列出所有 float/double/long double 重载(我的重点,并转述)之后:

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

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


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

因为在 CC++ 创建的日子里,我既没有与他们密切相关(虽然我相当老了),也不是 ANSI/ISO 委员会的成员创建标准,这必然是我的观点。我想这是知情的意见,但是,正如我妻子会告诉你的那样(经常而且不需要太多鼓励),我以前错了:-)

假设,不管它的价值是什么。

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

其次,您必须记住的另一件事是,C 的初衷是作为一种系统 编程语言,浮点在该领域是否可取值得怀疑。 p>

由于它最初的用例之一是编写 UNIX 代码,因此浮点数几乎毫无用处。 C 所基于的 BCPL 也没有使用幂(从内存中它根本没有浮点)。

顺便说一句,积分幂运算符可能是二元运算符而不是库调用。您不用 x = add (y, z) 而是用 x = y + z 添加两个整数 - 语言本身的一部分,而不是库。

第三,由于积分幂的实现相对微不足道,几乎可以肯定该语言的开发人员会更好地利用他们的时间来提供更多有用的东西(见下文关于机会成本的 cmets)。

这也与原始 C++ 有关。由于最初的实现实际上只是一个产生C 代码的翻译器,它继承了C 的许多属性。它最初的意图是 C-with-classes,而不是 C-with-classes-plus-a-little-bit-of-extra-math-stuff。

至于为什么在C++11 之前从未将其添加到标准中,您必须记住,标准制定机构有具体的指导方针要遵循。例如,ANSI C 专门负责编纂现有实践,而不是创建新语言。否则,他们可能会发疯并给我们 Ada :-)

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

例如,C99 基本原理文档特别继承了 C89 指导原则中的两个限制可以添加的内容:

保持语言简洁明了。 只提供一种操作方式。

指南(不一定是那些特定的指南)是为各个工作组制定的,因此也限制了C++ 委员会(以及所有其他 ISO 组)。

此外,标准制定机构意识到他们做出的每一个决定都有一个机会成本(一个经济术语,意思是你必须放弃什么才能做出决定)。例如,购买价值 10,000 美元的超级游戏机的机会成本是与您的另一半保持约 6 个月的亲切关系(或可能所有关系)。

Eric Gunnerson 用他的-100 points explanation 很好地解释了为什么不总是将东西添加到 Microsoft 产品中 - 基本上,一个功能从 100 分开始,所以它必须增加相当多的价值才能被考虑。

换句话说,您宁愿在标准中添加一个完整的电源运算符(老实说,任何半体面的编码员都可以在十分钟内完成)还是多线程?就我自己而言,我更喜欢后者,而不必纠结于 UNIX 和 Windows 下的不同实现。

我还希望看到成千上万的标准库(散列、btree、红黑树、字典、任意映射等)集合,但是,正如基本原理所述:

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

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

无论如何,这是我对此事的(相当冗长的)想法。如果只根据数量而不是质量来投票,我很快就会把其他所有人都吓跑了。感谢收听:-)

【讨论】:

FWIW,我不认为 C++ 遵循“仅提供一种操作方式”作为约束。没错,因为例如 to_string 和 lambdas 都是您已经可以做的事情的便利。我想人们可以非常松散地解释“只有一种方式来进行操作”以允许这两种方式,同时允许人们可以想象的几乎所有功能的重复,通过说“啊哈!不!因为便利性使它与精确等效但更冗长的替代方案略有不同!”。 lambda 肯定是这样。 @Steve,是的,我的措辞很糟糕。更准确地说,每个委员会都有指导方针,而不是所有委员会都遵循相同的指导方针。调整后的答案澄清 只有一点(在少数中):“任何代码猴子都可以在十分钟内完成”。当然,如果每年有 100 只代码猴子(这是一个侮辱性的术语,顺便说一句)这样做(可能是一个较低的估计),我们就浪费了 1000 分钟。非常高效,你不觉得吗? @Jürgen ,这并不是侮辱性的(因为我实际上并没有将标签归于任何特定的人),这只是表明pow 并不需要太多技巧.当然,我宁愿让标准提供一些需要大量技能的东西,如果必须重复努力,则会浪费更多的时间。 @eharo2,只需将当前文本中的“半体面的编码器”替换为“代码猴子”即可。我也不认为这是侮辱,但我认为最好保持谨慎,老实说,目前的措辞传达了同样的想法。【参考方案2】:

对于任何固定宽度的整数类型,几乎所有可能的输入对都会溢出类型。如果函数的绝大多数可能输入都没有给出有用的结果,那么标准化它有什么用?

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


编辑:在对该问题的评论中,static_rtti 写道:“大多数输入导致它溢出?exp 和 double pow 也是如此,我没有看到有人抱怨。”这是不正确的。

让我们抛开exp,因为这无关紧要(尽管它实际上会使我的情况更强大),并专注于double pow(double x, double y)。对于 (x,y) 对的哪一部分,这个函数做了一些有用的事情(即,不仅仅是上溢或下溢)?

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

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

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

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

【讨论】:

无论如何这一切都没有实际意义。仅仅因为一个函数对某些或很多输入无效并不会降低它的用处。 @static_rtti: pow(x,y) 不会下溢到零,如果 |y| 非常 窄的输入带(大 x,y 非常接近 -1)发生下溢,但结果在该范围内仍然有意义。 经过深思熟虑,我同意下溢。不过,我仍然认为这与问题无关。 @ybungalobill:你为什么选择这个作为理由?就个人而言,我倾向于对大量问题和程序员有用,可以制作比大多数程序员可能编写的幼稚实现更快的硬件优化版本,等等。您的标准似乎完全武断,坦率地说,毫无意义。 @StephenCanon:从好的方面来说,你的论点表明整数pow 的明显正确和最优的实现只是一个很小的查找表。 :-)【参考方案3】:

因为无论如何都无法在 int 中表示所有整数幂:

>>> print 2**-4
0.0625

【讨论】:

对于有限大小的数值类型,由于溢出,无法表示该类型中该类型的所有幂。但你关于负面权力的观点更有效。 我认为负指数是标准实现可以处理的,要么将无符号整数作为指数,要么在提供负指数作为输入且 int 是预期输出时返回零。跨度> 或者有单独的int pow(int base, unsigned int exponent)float pow(int base, int exponent) 他们可以将其声明为未定义的行为来传递一个负整数。 在所有现代实现中,int pow(int base, unsigned char exponent) 之外的任何东西无论如何都有些无用。底数为 0 或 1,指数无关紧要,它是 -1,在这种情况下只有指数的最后一位很重要,或者base >1 || base< -1 在这种情况下exponent<256 会受到溢出的惩罚。【参考方案4】:

这实际上是一个有趣的问题。我在讨论中没有发现的一个论点是简单地缺乏明显的参数返回值。让我们来计算一下假设的int pow_int(int, int) 函数可能失败的方式。

    溢出 结果未定义pow_int(0,0) 结果无法表示pow_int(2,-1)

该函数至少有 2 种故障模式。整数不能表示这些值,函数在这些情况下的行为需要由标准定义 - 程序员需要知道函数如何准确处理这些情况。

总体而言,将功能排除在外似乎是唯一明智的选择。程序员可以使用带有所有可用错误报告的浮点版本。

【讨论】:

但是前两种情况不也适用于浮点数之间的pow 吗?拿两个大花车,将一个提升到另一个的力量,你有一个溢出。 pow(0.0, 0.0) 会导致与您的第二点相同的问题。您的第三点是实现整数与浮点数的幂函数之间的唯一真正区别。【参考方案5】:

简答:

pow(x, n) 特化为n 是自然数的地方通常对于时间性能很有用。但是标准库的通用 pow() 仍然可以很好地用于此目的(令人惊讶!),并且尽可能少地包含在标准 C 库中以使其具有可移植性和尽可能容易实现。另一方面,这并不能阻止它出现在 C++ 标准库或 STL 中,我很确定没有人打算在某种嵌入式平台中使用它们。

现在,答案很长。

pow(x, n) 在许多情况下可以通过将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 上测试了一亿次迭代,x 等于迭代次数,n 等于 10。在这种情况下,pown_l 获胜。 glibc pow() 耗时 12.0 秒,pown 耗时 7.4 秒,pown_l 仅耗时 6.5 秒。所以这并不奇怪。我们或多或少对此有所期待。

然后,我让 x 保持不变(我将其设置为 2.5),然后我将 n 从 0 循环到 19 亿次。这一次,出乎意料的是,glibc pow 赢了,而且以压倒性优势获胜!只用了 2.0 用户秒。我的pown 用了 9.6 秒,pown_l 用了 12.2 秒。这里发生了什么?我做了另一个测试来找出答案。

我做了与上面相同的事情,只是x 等于一百万。这一次,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 函数调用并用适当的选择替换它三种算法中。 (现在,如果你真的要尝试使用这个,你可能不得不玩弄它,因为我没有完全尝试编译我的'd 写在上面。;))

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

脚注:在时间性能测试中,我为我的函数提供了相对慷慨的优化标志 (-s -O2),这些优化标志可能与在我的系统 (archlinux) 上编译 glibc 所使用的相当,甚至更差,所以结果可能是公平的。对于更严格的测试,我必须自己编译 glibc,而我reeeally 不想这样做。我曾经使用过 Gentoo,所以我记得它需要多长时间,即使任务是自动化。结果对我来说是决定性的(或者说是不确定的)。当然欢迎你自己做。

奖励回合:如果需要精确的整数输出,pow(x, n) 对所有整数的特化是instrumental,这确实发生了。考虑为具有 p^N 个元素的 N 维数组分配内存。将 p^N 减去 1 也会导致可能随机发生的段错误。

【讨论】:

我想如果你摆脱递归,你将节省堆栈分配所需的时间。是的,我们遇到了这样一种情况,即 pow 会降低一切速度,我们必须实施自己的 pow。 “没有人有如此极端的内存限制”是错误的。 PIC 通常有一个有限的调用堆栈,最多 3 个(例如 PIC10F200)到 8 个(例如 16F722A)调用(PIC 使用硬件堆栈进行函数调用)。 哦,伙计,这太残酷了,哈哈。好的,所以它不适用于那些 PIC。 对于整数基数和幂,就像问题所问的那样,编译器(gcc 和 clang)很容易从迭代(而不是递归)实现中生成无分支循环。这避免了来自n 的每一位的分支错误预测。 godbolt.org/z/L9Kb98。 gcc 和 clang 未能将您的递归定义优化为一个简单的循环,实际上在 n 的每一位上进行分支。 (对于pown_iter(double,unsigned),它们仍然分支,但在 x86 asm 或 C 内部函数中应该可以实现无分支 SSE2 或 SSE4.1。但即使这样也比递归更好) 废话,现在我必须使用基于循环的版本重新进行基准测试才能确定。我会考虑的。【参考方案6】:

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

获得稍微准确的结果也意味着获得稍微不同的结果。

【讨论】:

【参考方案7】:

世界在不断发展,编程语言也在不断发展。 fourth part of the C decimal TR¹ 为 &lt;math.h&gt; 添加了更多功能。这个问题可能对这些函数的两个系列感兴趣:

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

似乎标准人员最终认为这些功能足够有用,可以集成到标准库中。但是,合理的是,ISO/IEC/IEEE 60559:2011 标准推荐这些函数用于二进制和十进制浮点数。我不能确定 C89 时遵循了什么“标准”,但 &lt;math.h&gt; 的未来演进可能会受到 ISO/IEC/IEEE 60559未来演进的严重影响> 标准。

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


¹您可以找到一些正在进行的文档here。

【讨论】:

是否有任何合理的实现,其中使用指数大于LONG_MAXpown 应该产生与使用LONG_MAX 不同的值,或者小于LONG_MIN 的值应该产生与LONG_MIN 不同的值?我想知道将intmax_t 用作指数有什么好处? @supercat 不知道,抱歉。 值得一提的是,从标准来看,它似乎还定义了一个可选的“crpown”函数,如果定义,它将是“pown”的正确舍入版本;本标准未指定所需的准确度。实现快速且适度精确的“pown”很容易,但确保在所有情况下都正确舍入往往要昂贵得多。【参考方案8】:

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

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;

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

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

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

【讨论】:

【参考方案9】:

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

(一方面,对数会降低乘法的幂,但整数的对数会在大多数输入中损失很多精度)

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

【讨论】:

我不知道有任何当前架构具有用于pow 的 FPU 指令。 x86 有一个y log2 x 指令(fyl2x),可以用作pow 函数的第一部分,但是以这种方式编写的pow 函数需要数百个周期才能在当前硬件上执行;编写良好的整数求幂例程要快几倍。 我不知道“数百”是准确的,在大多数现代 CPU 上,fyl2x 和 f2xm1 似乎大约是 150 个周期,并且与其他指令一起流水线化。但是你是对的,一个经过良好调整的整数实现应该更快(这些天),因为 IMUL 的速度比浮点指令快得多。不过,在编写 C 标准时,IMUL 相当昂贵,而且在循环中使用它可能确实比使用 FPU 花费更长的时间。 根据更正更改了我的投票;不过,请记住(a)C 标准在 1999 年经历了一次重大修订(包括数学库的大规模扩展),并且(b)C 标准没有针对任何特定的处理器架构编写——存在或者 x86 上没有 FPU 指令基本上与 C 委员会选择标准化的功能无关。 它不依赖于任何架构,是的,但与整数乘法相比,查找表插值(通常用于浮点实现)的相对成本对于我猜想的所有架构来说变化几乎相同.【参考方案10】:

事实上,确实如此。

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


编辑:纯粹主义者可能会认为这是不正确的,因为实际上使用了“提升”类型。一种或另一种方式,在int 参数上获得正确的int 结果或错误。

【讨论】:

这是不正确的。 (7) 重载是pow ( Arithmetic1 base, Arithmetic2 exp ),如果您已阅读说明,它将被转换为doublelong double"7) 一组重载或用于算术参数的所有组合的函数模板1-3) 未涵盖的类型。如果任何参数为整数类型,则将其强制转换为 double。如果任何参数为 long double,则返回类型 Promoted 也为 long double,否则返回类型始终为 double。"我> 这里有什么问题?我只是说现在(自 C++11 起)模板化的 pow(,) 在标准库中,而 2010 年并非如此。 不,不是。 Templeates 将这些类型提升为 double 或 long double。所以它适用于下面的双打。 @Trismegistos 它仍然允许 int 参数。如果此模板不存在,则传递 int 参数会导致它将 int 中的位解释为浮点数,从而导致任意意外结果。混合输入值也会发生同样的情况。例如pow(1.5f, 3) = 1072693280pow(1.5f, float(3)) = 3.375 OP 要求 int pow(int, int),但 C++ 11 仅提供 double pow(int, int)。请参阅@phuclv 的解释。【参考方案11】:

一个很简单的原因:

5^-2 = 1/25

STL 库中的所有内容都基于可以想象的最准确、最强大的内容。当然,int 会回到零(从 1/25 开始),但这是一个不准确的答案。

我同意,在某些情况下这很奇怪。

【讨论】:

显然需要一个无符号的第二个参数。有许多应用程序只需要非负整数幂。

以上是关于为啥标准 C++ 库中没有“int pow(int base, int exponent)”?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 C++ 标准库中没有 SIMD 功能?

为啥C++标准库中没有transform_if?

为啥 Python 的标准库中没有排序容器?

c++标准库中没有关于正则匹配字符串的函数么

如何在 C++ 标准库中更改堆中的最大元素?

为啥在 C++ 中没有强制内联的标准方法?