比较两个函数的执行时间?

时间:2019-08-10 04:02:55

标签: c

我写了两个功能相同的函数,但是使用了不同的算法。我使用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()也会产生相同的结果,这表明我的基准测试实现是错误的。

1 个答案:

答案 0 :(得分:7)

对这样的函数调用准确而有效地执行计时可能会令人惊讶地困难。这是您的程序的修改,说明了困难:

#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纳秒的函数大约可以完成23 / 0.45 = 45项操作。而且由于我的示例exponentFast函数所进行的乘法运算与指数的值大致相同,所以看起来我们可能正处于正确的位置。

我要确认我至少获得了合理的结果的另一件事是改变试验次数。将NTRIALS减少到1亿,整个程序只花费了大约十分之一的运行时间,这意味着每次调用的时间是一致的。

现在,要指出第2点,我仍然记得我作为程序员的一种形成经验,当我编写标准功能的新的和改进的版本时,我 knew 将会变得更快。 ,并花了几个小时对其进行调试以使其完全正常工作之后,我发现它的测量速度并没有明显提高,直到我将试验次数增加到数百万次,并且一分钱(正如他们所说)下降了。 >

但是正如我所说,到目前为止,我提出的结果是一个偶然的巧合,令人误解。当我第一次汇总一些简单的代码来改变赋予函数调用的参数时,如上所述,我有:

int base = 2;
int power = 25;

然后在循环内

    base = (base + 1) % 5;
    power = (power + 7) % 16;

这是为了允许base从0到4,power从0到15,选择数字以确保即使{{1} }是4。但这意味着base平均只有8,这意味着我精打细算的power通话平均只需经过8次行程,而不是您的25次原始帖子。

当我将迭代步骤更改为

exponentFast

-就是说,power = 25 + (power - 25 + 1) % 5; 不变(因此,使其保持为常数2),并且base的变化范围是25到30,即{{1 }}上升到大约63纳秒。好消息是,这是有道理的(平均而言,迭代大约是它的三倍,慢了大约三倍),但坏消息是,我的“ power”函数看起来并不快! (不过,显然,我没想到它具有简单的暴力循环。如果我想使其更快,我要做的第一件事就是应用,而无需付出太多的复杂性) "binary exponentiation"。)

不过,至少还有一件事要担心,那就是如果我们将这些函数调用十亿次,那么我们不仅要计算出每个函数完成工作所花费的时间十亿倍,而且函数调用开销的十亿倍。如果函数调用开销与该函数正在执行的工作量相等,则我们(a)很难测量实际的工作时间,但是(b)很难加快速度! (我们可以通过内联测试函数来消除函数调用的开销,但是,如果最终程序中对函数的实际使用将涉及实际的函数调用,那显然是没有意义的。)

还有一个不准确之处是,我们通过为每次呼叫计算exponentFast和/或exponentFast的新值和不同值,并将所有结果相加来引入时序工件。完成这项工作的摊销时间变成了我们所说的“每次通话时间”。 (至少这个问题,因为它同等地影响任一幂函数,因此不会影响我们评估哪一个更快)的能力。


附录:由于我的初始指数“ base确实很尴尬,而且二进制表达式 so 简单而优雅,因此我又进行了一次测试,重写了{{1 }}为

power

现在-万岁! -在我的计算机上,每次exponentFast的平均通话时间下降到大约16 ns。但是“万岁!”是合格的。显然,它比调用exponentFast快25%,这非常重要,但不是一个数量级或任何数量级。如果我正在使用该程序的所有时间都花在了指数上,那么我也会使该程序的速度提高25%,但如果不这样做,改进的幅度将较小。在某些情况下,改进(在程序的所有预期运行中节省的时间)少于我编写和测试自己的版本所花费的时间。而且我还没有花时间在改进的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; } 函数上进行适当的回归测试,但是如果这不是Stack Overflow帖子中的任何内容,那我就不得不这样做。它有几种极端情况,并且很可能包含潜伏的错误。