【问题标题】:Comparing execution time of two functions?比较两个函数的执行时间?
【发布时间】:2019-08-10 04:02:55
【问题描述】:

我写了两个函数,它们做同样的事情,但使用不同的算法。我用time.h中的clock()比较执行时间,结果不一致。

我试图改变函数的执行顺序,但似乎第一个执行的函数总是有更长的运行时间

#include <stdio.h>
#include <time.h>

long exponent(int a, int b);
long exponentFast(int a, int b);


int main(void)
{
    int base;
    int power;
    clock_t begin;
    clock_t end;
    double time_spent;

    base = 2;
    power = 25;

    begin = clock();
    long result1 = exponentFast(base, power);
    end = clock();
    time_spent = (double)(end - begin) / CLOCKS_PER_SEC;
    printf("Result1: %li, Time: %.9f\n", result1, time_spent);

    begin = clock();
    long result2 = exponent(base, power);
    end = clock();
    time_spent = (double)(end - begin) / CLOCKS_PER_SEC;
    printf("Result2: %li, Time: %.9f\n", result2, time_spent);
}

long exponent(int a, int b)
{
    ...
}

long exponentFast(int a, int b)
{
    ...
}

我预计 result1 的 time_spent 的值低于 result2 的值,但输出为

Result1: 33554432, Time: 0.000002000
Result2: 33554432, Time: 0.000001000

在 exponentFast() 之前执行 exponent() 也会产生相同的结果,这表明我的基准测试实现是错误的。

【问题讨论】:

  • Here's a similar question 可能对您有所帮助。
  • 最有可能的是,这两个函数都执行得太快,无法使单个调用的任何时间变得准确。您将需要多次运行每个函数。您应该考虑改变函数的参数;您应该旨在累积返回值,以便编译器不能简单地优化调用。当 CLOCKS_PER_SEC 为 1,000,000(POSIX 要求)时,测量 1 µs 与 2 µs 没有意义。因此,您应该考虑 100,000 到 1,000,000 次迭代。确保对两个函数使用相同的参数序列——比较苹果和苹果。
  • 此外,在实际测量之前对函数进行一些热身调用会有所帮助。你在什么操作系统上?大多数现代操作系统都具有更高的分辨率和准确的计时 API。
  • 尝试执行该函数大约百万次,这可能会产生更好的结果。

标签: c


【解决方案1】:

准确而显着地对此类函数调用执行计时可能非常困难。这是您的程序的修改,说明了困难:

#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

这里有两件大事需要注意。

  1. 我运行每个函数十亿次。没错,十亿,一亿。然而,这只花了大约 20 秒。
  2. 即使进行了这么多试验,“常规”和“快速”版本之间仍然几乎没有明显的区别。平均而言,一个需要 21 纳秒(纳秒!);另一个耗时 23 纳秒。大声呐喊。

(但实际上,第一次试验具有很大的误导性。稍后会详细介绍。)

在继续之前,有必要问几个问题。

  1. 这些结果是否有意义?这些函数真的有可能只花费几十纳秒来完成它们的工作吗?
  2. 假设它们是准确的,这些结果是否告诉我们我们在浪费时间,“常规”和“快速”版本之间的差异如此之小以至于不值得编写和调试“快”?

首先,让我们做一个粗略的分析。我的机器声称拥有 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 帖子,我必须这样做。它有几组边缘情况,很可能包含潜伏的错误。

【讨论】:

  • 光靠努力就值得 UV,加上很好的解释,可惜我没有 2。
猜你喜欢
  • 2022-07-05
  • 1970-01-01
  • 2014-10-12
  • 2022-06-19
  • 1970-01-01
  • 1970-01-01
  • 2012-02-04
  • 1970-01-01
  • 2019-08-13
相关资源
最近更新 更多