为什么函数的递归版本比C中的迭代版本更快?

时间:2011-12-22 17:36:12

标签: c++ c recursion iteration

我正在检查梯度下降的两个实现之间的区别,我的猜测是在编译器优化后,两个版本的算法都是等价的。

令我惊讶的是,递归版本明显更快。我没有丢弃任何版本的实际缺陷,甚至没有丢弃我测量时间的方式。你能告诉我一些见解吗?

这是我的代码:

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#include <stdint.h>

double f(double x)
{
        return 2*x;
}

double descgrad(double xo, double xnew, double eps, double precision)
{
//      printf("step ... x:%f Xp:%f, delta:%f\n",xo,xnew,fabs(xnew - xo));

        if (fabs(xnew - xo) < precision)
        {
                return xnew;
        }
        else
        {
                descgrad(xnew, xnew - eps*f(xnew), eps, precision);
        }
}

double descgraditer(double xo, double xnew, double eps, double precision)
{
        double Xo = xo;
        double Xn = xnew;

        while(fabs(Xn-Xo) > precision)
        {
                //printf("step ... x:%f Xp:%f, delta:%f\n",Xo,Xn,fabs(Xn - Xo));
                Xo = Xn;
                Xn = Xo - eps * f(Xo);
        }

        return Xn;
}

int64_t timespecDiff(struct timespec *timeA_p, struct timespec *timeB_p)
{
  return ((timeA_p->tv_sec * 1000000000) + timeA_p->tv_nsec) -
           ((timeB_p->tv_sec * 1000000000) + timeB_p->tv_nsec);
}

int main()
{
        struct timespec s1, e1, s2, e2;

        clock_gettime(CLOCK_MONOTONIC, &s1);
        printf("Minimum : %f\n",descgraditer(100,99,0.01,0.00001));
        clock_gettime(CLOCK_MONOTONIC, &e1);

        clock_gettime(CLOCK_MONOTONIC, &s2);
        printf("Minimum : %f\n",descgrad(100,99,0.01,0.00001));
        clock_gettime(CLOCK_MONOTONIC, &e2);

        uint64_t dif1 = timespecDiff(&e1,&s1) / 1000;
        uint64_t dif2 = timespecDiff(&e2,&s2) / 1000;

        printf("time_iter:%llu ms, time_rec:%llu ms, ratio (dif1/dif2) :%g\n", dif1,dif2, ((double) ((double)dif1/(double)dif2)));

        printf("End. \n");
}

我正在使用以下选项在Ubuntu 11.04上使用gcc 4.5.2进行编译:     gcc grad.c -O3 -lrt -o dg

我的代码输出是:

Minimum : 0.000487
Minimum : 0.000487
time_iter:127 ms, time_rec:19 ms, ratio (dif1/dif2) :6.68421
End.

我读了一个线程,它也询问算法的递归版本比迭代版本更快。对那里的解释是,使用堆栈的递归版本和使用一些向量的其他版本,堆上的访问正在减慢迭代版本。但在这种情况下(根据我的理解)我只是在两种情况下使用堆栈。

我错过了什么吗?有什么明显我没看到的吗?我测量时间的方式错了吗?任何见解?

编辑: 神秘在评论中解决了。正如@TonyK所说,printf的初始化减慢了第一次执行的速度。对不起,我错过了那个显而易见的事情。

顺便说一下,代码编译恰到好处而没有警告。我不认为“返回descgrad(..”是必要的,因为停止条件发生在之前。

7 个答案:

答案 0 :(得分:10)

我已在本地编译并运行您的代码。将printf移到定时块之外会使两个版本每次执行约5ms。

因此,你的计时中的一个核心错误就是你测量了printf的复杂野兽,它的运行时使你实际想要测量的代码相形见绌。

我的main() - 功能现在看起来像这样:

int main() {
    struct timespec s1, e1, s2, e2;

    double d = 0.0;

    clock_gettime(CLOCK_MONOTONIC, &s1);
    d = descgraditer(100,99,0.01,0.00001);
    clock_gettime(CLOCK_MONOTONIC, &e1);
    printf("Minimum : %f\n", d);

    clock_gettime(CLOCK_MONOTONIC, &s2);
    d = descgrad(100,99,0.01,0.00001);
    clock_gettime(CLOCK_MONOTONIC, &e2);
    printf("Minimum : %f\n",d);

    uint64_t dif1 = timespecDiff(&e1,&s1) / 1000;
    uint64_t dif2 = timespecDiff(&e2,&s2) / 1000;

    printf("time_iter:%llu ms, time_rec:%llu ms, ratio (dif1/dif2) :%g\n", dif1,dif2, ((double) ((double)dif1/(double)dif2)));

    printf("End. \n");
}

答案 1 :(得分:5)

  

我测量时间的方式错了吗?

是。在您测量的短时间范围内,调度程序可能会对您的程序产生巨大影响。您需要更长时间地进行测试以平均这些差异,或者使用CLOCK_PROCESS_CPUTIME_ID来衡量流程使用的CPU时间。

答案 2 :(得分:3)

首先,您的递归步骤错过了return

double descgrad(double xo, double xnew, double eps, double precision)
{
    if (fabs(xnew - xo) < precision)
        return xnew;
    else
        descgrad(xnew, xnew - eps*f(xnew), eps, precision);
}

应该是:

double descgrad(double xo, double xnew, double eps, double precision)
{
    if (fabs(xnew - xo) < precision)
        return xnew;
    else
        return descgrad(xnew, xnew - eps*f(xnew), eps, precision);
}

这种疏忽导致descgrad的返回值未定义,因此编译器几乎不必为其生成代码;)

答案 3 :(得分:2)

首先,您在尝试测量时包含了printf。它始终是一个巨大的禁忌,因为它可以,并且很可能会在执行控制台输出时暂停您的进程。实际上,做任何系统调用都可以完全抛弃这些时间测量。

其次,正如其他人提到的那样,在短暂的采样周期内,调度程序中断会产生巨大的影响。

这不是完美的,但试试这个为你的主要,你会发现实际上差别很小。随着循环次数的增加,比率接近1.0。

#define LOOPCOUNT 100000
int main() 
{
    struct timespec s1, e1, s2, e2;
    int i;
    clock_gettime(CLOCK_MONOTONIC, &s1);
    for(i=0; i<LOOPCOUNT; i++)
    {
      descgraditer(100,99,0.01,0.00001);
    }
    clock_gettime(CLOCK_MONOTONIC, &e1);

    clock_gettime(CLOCK_MONOTONIC, &s2);
    for(i=0; i<LOOPCOUNT; i++)
    {
      descgrad(100,99,0.01,0.00001);
    }
    clock_gettime(CLOCK_MONOTONIC, &e2);

    uint64_t dif1 = timespecDiff(&e1,&s1) / 1000;
    uint64_t dif2 = timespecDiff(&e2,&s2) / 1000;

    printf("time_iter:%llu ms, time_rec:%llu ms, ratio (dif1/dif2) :%g\n", dif1,dif2, ((double) ((double)dif1/(double)dif2)));

    printf("End. \n");

}

编辑:在使用objdump -dS查看反汇编输出后,我注意到了一些事情:
   通过-O3优化,上面的代码完全优化了函数调用。但是,它仍然会为这两个函数生成代码,实际上它们都不是递归的。

其次,使用-O0,使得结果代码实际上是递归的,递归版本实际上万亿倍慢。我的猜测是因为调用堆栈强制变量最终在内存中迭代版本用完寄存器和/或缓存。

答案 4 :(得分:2)

接受的答案不正确

迭代函数和递归函数的运行时间存在差异,原因是-foptimize-sibling-calls添加了编译器优化-O3

首先,代码:

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#include <stdint.h>

double descgrad(double xo, double xnew, double eps, double precision){
        if (fabs(xnew - xo) <= precision) {
                return xnew;
        } else {
                return descgrad(xnew, xnew - eps*2*xnew, eps, precision);
        }
}

double descgraditer(double xo, double xnew, double eps, double precision){
        double Xo = xo;
        double Xn = xnew;

        while(fabs(Xn-Xo) > precision){
                Xo = Xn;
                Xn = Xo - eps * 2*Xo;
        }
        return Xn;
}

int main() {
        time_t s1, e1, d1, s2, e2, d2;
        int i, iter = 10000000;
        double a1, a2;

        s1 = time(NULL);
        for( i = 0; i < iter; i++ ){
            a1 = descgraditer(100,99,0.01,0.00001);
        }
        e1 = time(NULL);
        d1 = difftime( e1, s1 );

        s2 = time(NULL);
        for( i = 0; i < iter; i++ ){
            a2 = descgrad(100,99,0.01,0.00001);
        }
        e2 = time(NULL);
        d2 = difftime( e2, s2 );

    printf( "time_iter: %d s, time_rec: %d s, ratio (iter/rec): %f\n", d1, d2, (double)d1 / d2 ) ;
    printf( "return values: %f, %f\n", a1, a2 );
}

以前的帖子指出你需要多次迭代才能平均环境干扰。鉴于此,我放弃了您的差分函数,转而使用time.h difftime time_t数据上的fabs(xnew-xo) < precision函数,因为在多次迭代中,任何比一秒更精细的东西都是无意义的。另外,我删除了基准测试中的printfs。

我还修复了递归实现中的错误。您的原始代码检查了-O3的if语句,这是不正确的(或者至少与迭代实现不同)。 fabs()&gt;迭代循环精度,因此当fabs &lt; = 精度时,递归函数不应递归。添加&#39;迭代&#39;两个函数的计数器确认此修复使函数在逻辑上等效。

使用$ gcc test.c -O3 -lrt -o dg $ ./dg time_iter: 34 s, time_rec: 0 s, ratio (iter/rec): inf return values: 0.000487, 0.000487 编译并运行:

-O3

在没有$ gcc test.c -lrt -o dg $ ./dg time_iter: 54 s, time_rec: 90 s, ratio (iter/rec): 0.600000 return values: 0.000487, 0.000487

的情况下进行编译和运行
-O3

在没有优化的情况下,迭代执行比递归更好。

然而,在-foptimize-sibling-calls优化下,递归在一秒钟内运行了一千万次迭代。原因是它添加了-O3,它优化了兄弟和尾递归调用,这正是你的递归函数正在利用的。

可以肯定的是,除了-foptimize-sibling-calls之外,所有$ gcc test.c -lrt -o dg -fcprop-registers -fdefer-pop -fdelayed-branch -fguess-branch-probability -fif-conversion2 -fif-conversion -fipa-pure-const -fipa-reference -fmerge-constants -ftree-ccp -ftree-ch -ftree-copyrename -ftree-dce -ftree-dominator-opts -ftree-dse -ftree-fre -ftree-sra -ftree-ter -funit-at-a-time -fthread-jumps -falign-functions -falign-jumps -falign-loops -falign-labels -fcaller-saves -fcrossjumping -fcse-follow-jumps -fcse-skip-blocks -fdelete-null-pointer-checks -fexpensive-optimizations -fgcse -fgcse-lm -fpeephole2 -fregmove -freorder-blocks -freorder-functions -frerun-cse-after-loop -fsched-interblock -fsched-spec -fschedule-insns -fschedule-insns2 -fstrict-aliasing -ftree-pre -ftree-vrp -finline-functions -funswitch-loops -fgcse-after-reload -ftree-vectorize $ ./dg time_iter: 55 s, time_rec: 89 s, ratio (iter/rec): 0.617978 return values: 0.000487, 0.000487 次优化都会运行它:

int a = 0;
int b = 0;

double descgrad(double xo, double xnew, double eps, double precision){
        if (fabs(xnew - xo) <= precision) {
                return xnew;
        } else {
                a++;
                return descgrad(xnew, xnew - eps*2*xnew, eps, precision);
        }
}

double descgraditer(double xo, double xnew, double eps, double precision){
        double Xo = xo;
        double Xn = xnew;

        while(fabs(Xn-Xo) > precision){
                b++;
                Xo = Xn;
                Xn = Xo - eps * 2*Xo;
        }
        return Xn;
}

int main() {
    time_t s1, e1, d1, s2, e2, d2;
    int i, iter = 10000000;
    double a1, a2;

    s1 = time(NULL);
    for( i = 0; i < iter; i++ ){
        a1 = descgraditer(100,99,0.01,0.00001);
    }
    e1 = time(NULL);
    d1 = difftime( e1, s1 );

    s2 = time(NULL);
    for( i = 0; i < iter; i++ ){
        a2 = descgrad(100,99,0.01,0.00001);
    }
    e2 = time(NULL);
    d2 = difftime( e2, s2 );

    printf( "time_iter: %d s, time_rec: %d s, ratio (iter/rec): %f\n", d1, d2, (double)d1 / d2 ) ;
    printf( "return values: %f, %f\n", a1, a2 );
    printf( "number of recurs/iters: %d, %d\n", a, b );
}

递归,没有尾调用优化,执行比迭代更差,与使用NO优化编译时相同。 Read about compiler optimizations here

编辑:

作为验证正确性,我更新了我的代码包括返回值。另外,我将两个静态变量设置为0,并在递归和迭代时逐递增,以验证输出是否正确:

$ gcc optimization.c -O3 -lrt -o dg
$ ./dg
time_iter: 41 s, time_rec: 24 s, ratio (iter/rec): 1.708333
return values: 0.000487, 0.000487
number of recurs/iters: 1755032704, 1755032704

输出:

{{1}}

答案是一样的,重复是一样的。

另外值得注意的是,静态变量提取/递增对尾部调用优化有相当大的影响,但是递归仍然超出迭代次数。

答案 5 :(得分:1)

首先,clock_gettime似乎是在测量挂钟时间,而不是 执行时间处理时间。其次,你测量的实际时间是 执行时间printf,而不是函数的执行时间。 第三,第一次调用printf时,它不在内存中,所以它 必须被分页,涉及重要的磁盘IO。颠倒顺序 你运行测试,结果也会反过来。

如果您想获得一些重要的测量值,您必须确保

  1. 只有您要测量的代码位于测量循环中, 或者至少,与你的相比,附加代码是非常小的 测量,
  2. 你对结果做了些什么,所以编译器不能 优化所有代码(在测试中不是问题),
  3. 你执行大量的测量代码 时间,取平均值,
  4. 您测量CPU时间,而不是挂钟时间,
  5. 你确保在开始之前所有内容都被分页 测量结果。

答案 6 :(得分:0)

在许多情况下,现代硬件缓存未命中是小循环结构的性能限制因素。递归实现不太可能在指令路径上创建缓存未命中。