准确而显着地对此类函数调用执行计时可能非常困难。这是您的程序的修改,说明了困难:
#include <stdio.h>
#include <time.h>
#include <math.h>
long exponent(int a, int b);
long exponentFast(int a, int b);
void tester(long (*)(int, int));
#define NTRIALS 1000000000
int main(void)
{
clock_t begin;
clock_t end;
double time_spent;
begin = clock();
tester(exponentFast);
end = clock();
time_spent = (double)(end - begin) / CLOCKS_PER_SEC;
printf("exponentFast: Time: %.9f = %.10f/call\n", time_spent, time_spent / NTRIALS);
begin = clock();
tester(exponent);
end = clock();
time_spent = (double)(end - begin) / CLOCKS_PER_SEC;
printf("exponent: Time: %.9f = %.10f/call\n", time_spent, time_spent / NTRIALS);
}
void tester(long (*func)(int, int))
{
int base = 2;
int power = 25;
int i;
unsigned long accum = 0;
for(i = 0; i < NTRIALS; i++) {
accum += (*func)(base, power);
base = (base + 1) % 5;
power = (power + 7) % 16;
}
printf("(accum = %lu)\n", accum);
}
long exponent(int a, int b)
{
return pow(a, b);
}
long exponentFast(int a, int b)
{
long ret = 1;
int i;
for(i = 0; i < b; i++)
ret *= a;
return ret;
}
你会注意到:
- 我已安排进行多次试验,其中包括添加一个新函数
tester() 来执行此操作。 (tester() 因此需要一个指向正在测试的函数的指针,这是一种您可能还不熟悉的技术。)
- 我已经安排在调用之间改变被测函数的参数。
- 我已经安排对被测函数的返回值做一些事情,即将它们全部加起来。
(第二个和第三个项目符号遵循 Jonathan Leffler 的建议,旨在确保过于聪明的编译器不会优化部分或全部有趣的工作。)
当我在我的电脑(一台普通的消费级笔记本电脑)上运行它时,我得到了以下结果:
(accum = 18165558496053920)
exponentFast: Time: 20.954286000 = 0.0000000210/call
(accum = 18165558496053920)
exponent: Time: 23.409001000 = 0.0000000234/call
这里有两件大事需要注意。
- 我运行每个函数十亿次。没错,十亿,一亿。然而,这只花了大约 20 秒。
- 即使进行了这么多试验,“常规”和“快速”版本之间仍然几乎没有明显的区别。平均而言,一个需要 21 纳秒(纳秒!);另一个耗时 23 纳秒。大声呐喊。
(但实际上,第一次试验具有很大的误导性。稍后会详细介绍。)
在继续之前,有必要问几个问题。
- 这些结果是否有意义?这些函数真的有可能只花费几十纳秒来完成它们的工作吗?
- 假设它们是准确的,这些结果是否告诉我们我们在浪费时间,“常规”和“快速”版本之间的差异如此之小以至于不值得编写和调试“快”?
首先,让我们做一个粗略的分析。我的机器声称拥有 2.2 GHz CPU。这意味着(粗略地说)它每秒可以做 22 亿件事情,或者每件事情大约 0.45 纳秒。所以一个需要 21 纳秒的函数可以做大约 21 ÷ 0.45 = 46 件事。而且由于我的示例 exponentFast 函数执行的乘法次数与指数值大致相同,因此看起来我们可能处于正确的位置。
为了确认我至少获得了准合理的结果,我所做的另一件事是改变试验次数。随着NTRIALS 减少到 100000000,整个程序的运行时间刚好是十分之一,这意味着每次调用的时间是一致的。
现在,到第 2 点,我仍然记得我作为程序员的一个成长经历,当时我编写了一个标准函数的新改进版本,我只是知道会变得更快,在花了几个小时调试它以使其完全正常工作后,我发现它并没有明显更快,直到我将试验次数增加到数百万,并且便士(正如他们所说)下降了。
但正如我所说,到目前为止,我所展示的结果是一种有趣的巧合,具有误导性。当我第一次拼凑一些简单的代码来改变函数调用的参数时,如上所示,我有:
int base = 2;
int power = 25;
然后,在循环中
base = (base + 1) % 5;
power = (power + 7) % 16;
这是为了让 base 从 0 到 4,power 从 0 到 15,选择数字以确保即使 base 为 4 时结果也不会溢出。但这意味着 power 平均只有 8 次,这意味着我的简单的 exponentFast 呼叫平均只需通过其循环进行 8 次旅行,而不是您原来帖子中的 25 次。
当我将迭代步骤更改为
power = 25 + (power - 25 + 1) % 5;
--也就是说,不改变 base(因此允许它保持为常数 2)并允许 power 在 25 和 30 之间变化,现在每次调用 exponentFast 的时间上升到大约 63纳秒。好消息是这是有道理的(大约是迭代次数的三倍,平均来说,它慢了大约三倍),但坏消息是我的“exponentFast”函数看起来不是很快! (不过,很明显,我没想到它会如此简单,蛮力循环。如果我想让它更快,我要做的第一件事就是应用"binary exponentiation".)
不过,至少还有一件事需要担心,那就是,如果我们调用这些函数 10 亿次,我们不仅计算了每个函数完成其工作所需时间的 10 亿次,而且还计算了函数调用开销的十亿倍。如果函数调用开销与函数正在执行的工作量相当,我们将 (a) 很难测量实际工作时间,而且 (b) 很难加快速度! (我们可以通过内联测试的函数来消除函数调用开销,但如果最终程序中函数的实际使用涉及到真正的函数调用,这显然没有意义。)
还有一个不准确的地方是,我们通过为每次调用计算 base 和/或 power 的新值和不同值并将所有结果相加来引入计时工件,以便摊销时间完成这项工作涉及我们一直所说的“每次通话时间”。 (这个问题,至少,因为它同样影响任何一个幂函数,不会影响我们评估哪个更快的能力。)
附录:由于我最初的“exponentFast”指数实在是令人尴尬的简单,而且由于二进制求幂如此简单而优雅,我又进行了一次测试,将exponentFast 重写为
long exponentFast(int a, int b)
{
long ret = 1;
long fac = a;
while(1) {
if(b & 1) ret *= fac;
b >>= 1;
if(b == 0) break;
fac *= fac;
}
return ret;
}
现在——万岁! -- 在我的机器上,对exponentFast 的平均调用下降到每次调用大约 16 ns。但是“万岁!”是合格的。显然它比调用pow() 快了大约 25%,这非常重要,但不是一个数量级或其他任何东西。如果我使用它的程序花费所有时间求幂,我也会使该程序快 25%,但如果不是,则改进会更少。在某些情况下,改进(在所有预期的程序运行中节省的时间)将少于我编写和测试自己的版本所花费的时间。而且我还没有花任何时间对改进的exponentFast 函数进行适当的回归测试,但如果这不是 Stack Overflow 帖子,我必须这样做。它有几组边缘情况,很可能包含潜伏的错误。