案例1:
int fun(int num)
{
if (num == 1)
return 1;
else
return num * fun(num - 1);
}
案例2:
int fun(int num)
{
int i = 1;
do
{
i = i * num;
num--;
}
while (num);
return i;
}
我在面试问题中得到了上述问题并询问,哪一个更快,记忆力更少。我真的不知道,如何找到,哪一个更快,除了我只是通过计算代码行来猜测。但是,我认为,这不是一个正确的方法。 请有人帮助我,我应该考虑解决这类问题。
更新
我要求的一般情况不仅仅适用于上述情况。
答案 0 :(得分:3)
这取决于编译器和您正在使用的优化(一个好的编译器可以将第一个代码转换为迭代),但是,通常,第二个解决方案会更快并占用更少的内存(因为递归调用需要创建堆栈框架。)
答案 1 :(得分:3)
关于代码大小,我将第一个实现放入文件f1.c
,将第二个实现放入f2.c
。
$ gcc -c f1.c f2.c
$ size f1.o f2.o
__TEXT __DATA __OBJC others dec hex
123 0 0 0 123 7b f1.o
119 0 0 0 119 77 f2.o
$ gcc -O3 -c f1.c f2.c
$ size f1.o f2.o
__TEXT __DATA __OBJC others dec hex
372 0 0 0 372 174 f1.o
362 0 0 0 362 16a f2.o
$
请注意,两种实现的代码大小差异很小。但有趣的是,优化后的代码比未经优化的代码要大得多(大约三倍)。
(编译:Mac OS X 10.7.5上的GCC 4.7.1。)
您应该注意到阶乘(这些功能实现的因素)增长非常快。事实上,13!太大了,不适合32位无符号整数,21!太大了,不适合64位无符号整数,35!太大了,无法容纳128位无符号整数(如果你能找到一台具有这种类型的计算机)。
另外,要注意假设。我希望迭代解决方案比递归解决方案更快。但是,测量表明不然。
测试是在配备2.3 GHz Intel Core i7(和16 GiB内存,但内存不是此计算中的因素)的MacBook Pro上运行的。
测量显示,当代码优化时,递归解决方案始终比纯迭代解决方案快一点,这与我的预期完全相反,但显示了为什么性能测量是必要的。
# iteration
# Count = 10
# Mean = 0.799869
# Variance = 0.000011
# recursion
# Count = 10
# Mean = 0.750904
# Variance = 0.000014
我后来添加了一个查找表函数,其时间是:
# lookuptab
# Count = 10
# Mean = 0.213836
# Variance = 0.000004
我添加了一个函数,只返回其输入参数来测量测试工具开销,并给出:
# over-head
# Count = 10
# Mean = 0.211325
# Variance = 0.000001
因此,阵列查找的计算成本非常小。
如果您曾怀疑优化器的功能,那么将优化时间与未经优化的构建进行比较。
# iteration
# Count = 10
# Mean = 1.852833
# Variance = 0.000020
# recursion
# Count = 10
# Mean = 2.937954
# Variance = 0.000059
查找表版本:
# lookuptab
# Count = 10
# Mean = 0.730275
# Variance = 0.000026
开销版本:
# over-head
# Count = 10
# Mean = 0.633132
# Variance = 0.000009
简单地计算代码行不是一个好的指导原因是不同的行具有不同的成本。例如,包含对sin()
,cos()
和tan()
等函数的调用的单行代码(可能)将比包含单个整数算术运算和赋值的20行代码昂贵得多。
当比较两个非常相似的函数时 - 如问题中那样 - 那么更复杂的递归往往比简单的迭代慢。但是,正如所展示的那样,当编译器设法优化时,这种猜测结果可能是错误的,特别是对于简单的尾递归函数,例如阶乘。
这是一个测试程序:
static int fun1(int num)
{
if (num == 1)
return 1;
else
return num * fun1(num - 1);
}
static int fun2(int num)
{
int i=1;
do{
i = i * num;
num--;
} while (num);
return i;
}
static int fun3(int num)
{
static const int factorial[] =
{ 1, 1, 2, 6, 24, 120, 720, 5040, 40320,
362880, 3628800, 39916800, 479001600,
};
enum { MAX_FACTORIAL_NUM = (sizeof(factorial)/sizeof(factorial[0])) };
if (num < 0 || num >= MAX_FACTORIAL_NUM)
return 0;
else
return factorial[num];
}
static int fun4(int num)
{
return num;
}
#include "timer.h"
#include <stdio.h>
static void tester(char const *name, int (*function)(int))
{
char buffer[32];
Clock clk;
unsigned long long sumfact = 0;
clk_init(&clk);
clk_start(&clk);
for (int i = 0; i < 100000000; i++)
sumfact += (*function)(i % 12 + 1);
clk_stop(&clk);
printf("%s: %s (%llu)\n", name, clk_elapsed_us(&clk, buffer, sizeof(buffer)), sumfact);
}
int main(void)
{
for (int i = 0; i < 10; i++)
{
tester("recursion", fun1);
tester("iteration", fun2);
tester("lookuptab", fun3);
tester("over-head", fun4);
}
return(0);
}
测试代码小心地将两个函数尽可能对称地对待,并交替测试每个函数以减少后台进程干扰性能的可能性。 (通常在后台运行的BOINC流程在这些测试中被关闭;对先前问题的时间经验表明它们严重影响结果并在结果中引入更多变化。)
-O3
)构建的原始时间没有查找表或头顶功能的程序的早期版本。
recursion: 0.754428 (4357969100681262)
iteration: 0.799330 (4357969100681262)
recursion: 0.749773 (4357969100681262)
iteration: 0.798897 (4357969100681262)
recursion: 0.747794 (4357969100681262)
iteration: 0.800977 (4357969100681262)
recursion: 0.748282 (4357969100681262)
iteration: 0.792708 (4357969100681262)
recursion: 0.748342 (4357969100681262)
iteration: 0.798776 (4357969100681262)
recursion: 0.748377 (4357969100681262)
iteration: 0.801641 (4357969100681262)
recursion: 0.750115 (4357969100681262)
iteration: 0.802468 (4357969100681262)
recursion: 0.750807 (4357969100681262)
iteration: 0.802829 (4357969100681262)
recursion: 0.751296 (4357969100681262)
iteration: 0.796841 (4357969100681262)
recursion: 0.759823 (4357969100681262)
iteration: 0.804221 (4357969100681262)
real 0m15.575s
user 0m15.556s
sys 0m0.027s
没有查找表或头顶功能的程序的早期版本。
recursion: 2.951282 (4357969100681262)
iteration: 1.852239 (4357969100681262)
recursion: 2.932758 (4357969100681262)
iteration: 1.851512 (4357969100681262)
recursion: 2.924796 (4357969100681262)
iteration: 1.862686 (4357969100681262)
recursion: 2.946792 (4357969100681262)
iteration: 1.846961 (4357969100681262)
recursion: 2.941705 (4357969100681262)
iteration: 1.849099 (4357969100681262)
recursion: 2.938599 (4357969100681262)
iteration: 1.852089 (4357969100681262)
recursion: 2.930713 (4357969100681262)
iteration: 1.854765 (4357969100681262)
recursion: 2.935669 (4357969100681262)
iteration: 1.851478 (4357969100681262)
recursion: 2.938975 (4357969100681262)
iteration: 1.856979 (4357969100681262)
recursion: 2.938250 (4357969100681262)
iteration: 1.850521 (4357969100681262)
real 0m47.980s
user 0m47.939s
sys 0m0.041s
我注意到两个阶乘函数的代码都存在错误;当被要求计算0!时,它们进入长时间运行的循环(并通过溢出32位int
类型来调用各种未定义的行为),这实际上定义良好且值为1.这就是调用的原因测试工具中的(*function)(i % 12 + 1)
而不是我最初写的(*function)(i % 13)
。
答案 2 :(得分:2)
(1)使用递归,并且可能是堆栈溢出的主题。 (2)是迭代的,并使用恒定的内存量。我会说(2)应该更快。
如果查看反汇编代码,(1)将有call
指令,这比仅增加/减少循环计数器更昂贵。但是,我相信如果你将1作为参数传递给函数,(1)可能会更快。如果参数大于1,则应该更快地执行(2)。
答案 3 :(得分:0)
循环比函数调用更简单,谈论它何时被编译为汇编。
您可以通过在调用一段代码之前和之后测量和比较时间戳和内存使用情况来测量所花费的时间和内存使用情况。
答案 4 :(得分:0)
迭代(没有递归的第二个)更快。
使用性能分析检查本文:http://www.codeproject.com/Articles/21194/Iterative-vs-Recursive-Approaches
答案 5 :(得分:0)
一个人使用更多内存的原因是,一个人在解决问题时会占用空间。问题不在于递归本身,因为递归可以实现为不使用堆栈空间(至少在某些语言中你可以使用“尾部”递归而不会受到惩罚)。但是,正如所写的那样,它不是尾递归的,递归调用的结果需要与活动调用的num
相乘(这可以写得更好,对不起)。