简答:
将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_l 在n 较低时表现最佳,pown 在x 较高时表现最佳。
所以这里有三种不同的算法,在适当的情况下,每种算法的性能都比其他算法更好。因此,最终,使用哪个最有可能取决于您计划如何使用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_expected 和 n_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 也会导致可能随机发生的段错误。